From 5ad83d74353d58edd4e090130a9513ecbf4364d6 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Sun, 18 Jan 2026 21:19:45 +0530 Subject: [PATCH 01/26] feat: refresh UI with new sidebar, header, and improved dialogs - Redesign sidebar with compact icon+label layout and OpNotes logo - Add hospital name and department to header in two rows - Remove animations from command palette and template picker for snappier UX - Add insert button with keyboard shortcut to template selector - Upgrade to Tailwind CSS v4 with custom theme configuration - Add theme switching support with multiple color themes - Add command palette for quick navigation (Cmd+K) - Add keyboard shortcuts help dialog - Create reusable loading skeleton components --- electron.vite.config.ts | 3 +- package.json | 142 +- pnpm-lock.yaml | 2028 ++++++++++------- pnpm-workspace.yaml | 4 + postcss.config.js | 5 +- src/main/api.ts | 6 +- src/main/repository/dashboard.ts | 115 + src/renderer/src/assets/opnotes-logo.png | Bin 0 -> 30394 bytes src/renderer/src/assets/opnotes-logo.svg | 33 + .../src/components/brand/OpNotesLogo.tsx | 178 ++ .../command-palette/CommandPalette.tsx | 211 ++ .../components/common/TemplateSelector.tsx | 34 +- .../keyboard/KeyboardShortcutsHelp.tsx | 70 + src/renderer/src/components/layout/Header.tsx | 68 + .../src/components/layout/MainLayout.tsx | 52 + .../src/components/layout/Sidebar.tsx | 78 + .../src/components/loading/CardSkeleton.tsx | 50 + .../components/loading/DashboardSkeleton.tsx | 57 + .../src/components/loading/TableSkeleton.tsx | 46 + src/renderer/src/components/loading/index.ts | 3 + .../src/components/theme/ThemeSwitcher.tsx | 171 ++ .../src/components/ui/alert-dialog.tsx | 2 +- src/renderer/src/components/ui/calendar.tsx | 47 +- src/renderer/src/components/ui/command.tsx | 16 +- .../components/ui/data-table/data-table.tsx | 112 +- src/renderer/src/components/ui/dialog.tsx | 2 +- src/renderer/src/components/ui/separator.tsx | 27 + src/renderer/src/components/ui/tooltip.tsx | 28 + src/renderer/src/contexts/ThemeContext.tsx | 206 ++ src/renderer/src/hooks/useDebounce.ts | 17 + .../src/hooks/useKeyboardShortcuts.ts | 153 ++ src/renderer/src/index.css | 647 +++++- src/renderer/src/lib/queries.ts | 10 + src/renderer/src/main.tsx | 2 + src/renderer/src/routes/home.tsx | 325 ++- src/renderer/src/routes/root.tsx | 77 +- src/renderer/src/styles/fonts.css | 88 + src/renderer/src/styles/themes.css | 346 +++ tailwind.config.js | 82 - tsconfig.node.json | 3 +- 40 files changed, 4287 insertions(+), 1257 deletions(-) create mode 100644 pnpm-workspace.yaml create mode 100644 src/main/repository/dashboard.ts create mode 100644 src/renderer/src/assets/opnotes-logo.png create mode 100644 src/renderer/src/assets/opnotes-logo.svg create mode 100644 src/renderer/src/components/brand/OpNotesLogo.tsx create mode 100644 src/renderer/src/components/command-palette/CommandPalette.tsx create mode 100644 src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx create mode 100644 src/renderer/src/components/layout/Header.tsx create mode 100644 src/renderer/src/components/layout/MainLayout.tsx create mode 100644 src/renderer/src/components/layout/Sidebar.tsx create mode 100644 src/renderer/src/components/loading/CardSkeleton.tsx create mode 100644 src/renderer/src/components/loading/DashboardSkeleton.tsx create mode 100644 src/renderer/src/components/loading/TableSkeleton.tsx create mode 100644 src/renderer/src/components/loading/index.ts create mode 100644 src/renderer/src/components/theme/ThemeSwitcher.tsx create mode 100644 src/renderer/src/components/ui/separator.tsx create mode 100644 src/renderer/src/components/ui/tooltip.tsx create mode 100644 src/renderer/src/contexts/ThemeContext.tsx create mode 100644 src/renderer/src/hooks/useDebounce.ts create mode 100644 src/renderer/src/hooks/useKeyboardShortcuts.ts create mode 100644 src/renderer/src/styles/fonts.css create mode 100644 src/renderer/src/styles/themes.css delete mode 100644 tailwind.config.js diff --git a/electron.vite.config.ts b/electron.vite.config.ts index d7f0cda..d7972a6 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,6 +1,7 @@ import { resolve } from 'path' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' export default defineConfig({ main: { @@ -17,7 +18,7 @@ export default defineConfig({ '@': resolve(__dirname, 'src/renderer/src') } }, - plugins: [react()], + plugins: [tailwindcss(), react()], build: { rollupOptions: { input: { diff --git a/package.json b/package.json index 9bd9147..2737480 100644 --- a/package.json +++ b/package.json @@ -28,81 +28,87 @@ "commitlint": "commitlint --edit" }, "dependencies": { - "@electron-toolkit/preload": "^3.0.0", + "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", - "@hookform/resolvers": "^3.3.4", + "@hookform/resolvers": "^3.9.1", "@lukemorales/query-key-factory": "^1.3.4", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-toggle": "^1.0.3", - "@tailwindcss/typography": "^0.5.12", - "@tanstack/react-query": "^5.34.1", - "@tanstack/react-query-devtools": "^5.34.1", - "@tanstack/react-table": "^8.16.0", - "@tiptap/extension-color": "^2.3.0", - "@tiptap/extension-placeholder": "^2.3.1", - "@tiptap/extension-text-align": "^2.4.0", - "@tiptap/extension-text-style": "^2.3.0", - "@tiptap/extension-underline": "^2.4.0", - "@tiptap/pm": "^2.3.0", - "@tiptap/react": "^2.3.0", - "@tiptap/starter-kit": "^2.3.0", - "better-sqlite3": "^9.5.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "dayjs": "^1.11.11", - "electron-log": "^5.1.4", - "electron-updater": "^6.1.8", + "@radix-ui/react-accordion": "^1.2.2", + "@radix-ui/react-alert-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toggle": "^1.1.1", + "@radix-ui/react-tooltip": "^1.1.6", + "@tanstack/react-query": "^5.62.8", + "@tanstack/react-query-devtools": "^5.62.8", + "@tanstack/react-table": "^8.20.6", + "@tiptap/extension-color": "^2.11.2", + "@tiptap/extension-placeholder": "^2.11.2", + "@tiptap/extension-text-align": "^2.11.2", + "@tiptap/extension-text-style": "^2.11.2", + "@tiptap/extension-underline": "^2.11.2", + "@tiptap/pm": "^2.11.2", + "@tiptap/react": "^2.11.2", + "@tiptap/starter-kit": "^2.11.2", + "better-sqlite3": "^11.7.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.4", + "date-fns": "^4.1.0", + "dayjs": "^1.11.13", + "electron-log": "^5.2.4", + "electron-updater": "^6.3.9", "handlebars": "^4.7.8", - "kysely": "^0.27.3", - "lucide-react": "^0.368.0", - "react-day-picker": "^8.10.0", - "react-hook-form": "^7.51.3", + "kysely": "^0.27.5", + "lucide-react": "^0.469.0", + "react-day-picker": "^9.4.4", + "react-hook-form": "^7.54.2", "react-hot-toast": "^2.4.1", - "react-router-dom": "^6.22.3", - "tailwind-merge": "^2.2.2", - "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.5" + "react-router-dom": "^7.1.1", + "tailwind-merge": "^2.6.0", + "zod": "^3.24.1" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3", + "electron", + "esbuild" + ] }, "devDependencies": { - "@commitlint/cli": "^19.3.0", - "@commitlint/config-conventional": "^19.2.2", + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.6.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0", - "@electron-toolkit/eslint-config-ts": "^1.0.1", + "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^1.0.1", - "@electron/rebuild": "^3.6.0", - "@tanstack/eslint-plugin-query": "^5.32.1", - "@types/better-sqlite3": "^7.6.10", - "@types/node": "^18.19.9", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.19", - "electron": "^28.2.0", - "electron-builder": "^24.9.1", - "electron-vite": "^2.2.0", - "eslint": "^8.56.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.2", - "husky": "^9.0.11", - "postcss": "^8.4.38", - "prettier": "^3.2.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwindcss": "^3.4.3", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vite-plugin-native": "^0.2.0" + "@electron/rebuild": "^3.7.1", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.0.0", + "@tanstack/eslint-plugin-query": "^5.62.8", + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.5", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "electron": "^33.3.1", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "eslint": "^8.57.1", + "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react-hooks": "^5.1.0", + "husky": "^9.1.7", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "vite": "^6.0.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6ef461..14c6ff3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,238 +9,231 @@ importers: .: dependencies: '@electron-toolkit/preload': - specifier: ^3.0.0 - version: 3.0.2(electron@28.3.3) + specifier: ^3.0.1 + version: 3.0.2(electron@33.4.11) '@electron-toolkit/utils': specifier: ^3.0.0 - version: 3.0.0(electron@28.3.3) + version: 3.0.0(electron@33.4.11) '@hookform/resolvers': - specifier: ^3.3.4 + specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.71.1(react@18.3.1)) '@lukemorales/query-key-factory': specifier: ^1.3.4 version: 1.3.4(@tanstack/query-core@5.90.19)(@tanstack/react-query@5.90.19(react@18.3.1)) '@radix-ui/react-accordion': - specifier: ^1.1.2 + specifier: ^1.2.2 version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-alert-dialog': - specifier: ^1.0.5 + specifier: ^1.1.4 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': - specifier: ^1.0.5 + specifier: ^1.1.4 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': - specifier: ^2.0.6 + specifier: ^2.1.4 version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': - specifier: ^1.3.0 + specifier: ^1.3.2 version: 1.3.2(react@18.3.1) '@radix-ui/react-label': - specifier: ^2.0.2 + specifier: ^2.1.1 version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': - specifier: ^1.0.7 + specifier: ^1.1.4 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-progress': - specifier: ^1.0.3 + specifier: ^1.1.1 version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': - specifier: ^2.0.0 + specifier: ^2.1.4 version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': - specifier: ^1.0.2 + specifier: ^1.1.1 version: 1.2.4(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-tabs': - specifier: ^1.1.13 + specifier: ^1.1.2 version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toggle': - specifier: ^1.0.3 + specifier: ^1.1.1 version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tailwindcss/typography': - specifier: ^0.5.12 - version: 0.5.19(tailwindcss@3.4.19) + '@radix-ui/react-tooltip': + specifier: ^1.1.6 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': - specifier: ^5.34.1 + specifier: ^5.62.8 version: 5.90.19(react@18.3.1) '@tanstack/react-query-devtools': - specifier: ^5.34.1 + specifier: ^5.62.8 version: 5.91.2(@tanstack/react-query@5.90.19(react@18.3.1))(react@18.3.1) '@tanstack/react-table': - specifier: ^8.16.0 + specifier: ^8.20.6 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/extension-color': - specifier: ^2.3.0 + specifier: ^2.11.2 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-text-style@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))) '@tiptap/extension-placeholder': - specifier: ^2.3.1 + specifier: ^2.11.2 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) '@tiptap/extension-text-align': - specifier: ^2.4.0 + specifier: ^2.11.2 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-text-style': - specifier: ^2.3.0 + specifier: ^2.11.2 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-underline': - specifier: ^2.4.0 + specifier: ^2.11.2 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/pm': - specifier: ^2.3.0 + specifier: ^2.11.2 version: 2.27.2 '@tiptap/react': - specifier: ^2.3.0 + specifier: ^2.11.2 version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/starter-kit': - specifier: ^2.3.0 + specifier: ^2.11.2 version: 2.27.2 better-sqlite3: - specifier: ^9.5.0 - version: 9.6.0 + specifier: ^11.7.0 + version: 11.10.0 class-variance-authority: - specifier: ^0.7.0 + specifier: ^0.7.1 version: 0.7.1 clsx: - specifier: ^2.1.0 + specifier: ^2.1.1 version: 2.1.1 cmdk: - specifier: ^1.0.0 + specifier: ^1.0.4 version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: - specifier: ^3.6.0 - version: 3.6.0 + specifier: ^4.1.0 + version: 4.1.0 dayjs: - specifier: ^1.11.11 + specifier: ^1.11.13 version: 1.11.19 electron-log: - specifier: ^5.1.4 + specifier: ^5.2.4 version: 5.4.3 electron-updater: - specifier: ^6.1.8 + specifier: ^6.3.9 version: 6.7.3 handlebars: specifier: ^4.7.8 version: 4.7.8 kysely: - specifier: ^0.27.3 + specifier: ^0.27.5 version: 0.27.6 lucide-react: - specifier: ^0.368.0 - version: 0.368.0(react@18.3.1) + specifier: ^0.469.0 + version: 0.469.0(react@18.3.1) react-day-picker: - specifier: ^8.10.0 - version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + specifier: ^9.4.4 + version: 9.13.0(react@18.3.1) react-hook-form: - specifier: ^7.51.3 + specifier: ^7.54.2 version: 7.71.1(react@18.3.1) react-hot-toast: specifier: ^2.4.1 version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-router-dom: - specifier: ^6.22.3 - version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.1.1 + version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: - specifier: ^2.2.2 + specifier: ^2.6.0 version: 2.6.0 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.19) zod: - specifier: ^3.23.5 + specifier: ^3.24.1 version: 3.25.76 devDependencies: '@commitlint/cli': - specifier: ^19.3.0 - version: 19.8.1(@types/node@18.19.130)(typescript@5.9.3) + specifier: ^19.6.1 + version: 19.8.1(@types/node@22.19.7)(typescript@5.9.3) '@commitlint/config-conventional': - specifier: ^19.2.2 + specifier: ^19.6.0 version: 19.8.1 '@electron-toolkit/eslint-config-prettier': specifier: ^2.0.0 version: 2.0.0(eslint@8.57.1)(prettier@3.8.0) '@electron-toolkit/eslint-config-ts': - specifier: ^1.0.1 - version: 1.0.1(eslint@8.57.1)(typescript@5.9.3) + specifier: ^3.1.0 + version: 3.1.0(eslint@8.57.1)(typescript@5.9.3) '@electron-toolkit/tsconfig': specifier: ^1.0.1 - version: 1.0.1(@types/node@18.19.130) + version: 1.0.1(@types/node@22.19.7) '@electron/rebuild': - specifier: ^3.6.0 + specifier: ^3.7.1 version: 3.7.2 + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.19(tailwindcss@4.1.18) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.1.18(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)) '@tanstack/eslint-plugin-query': - specifier: ^5.32.1 + specifier: ^5.62.8 version: 5.91.3(eslint@8.57.1)(typescript@5.9.3) '@types/better-sqlite3': - specifier: ^7.6.10 + specifier: ^7.6.12 version: 7.6.13 '@types/node': - specifier: ^18.19.9 - version: 18.19.130 + specifier: ^22.10.5 + version: 22.19.7 '@types/react': - specifier: ^18.2.48 + specifier: ^18.3.18 version: 18.3.27 '@types/react-dom': - specifier: ^18.2.18 + specifier: ^18.3.5 version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': - specifier: ^4.2.1 - version: 4.7.0(vite@5.4.21(@types/node@18.19.130)) - autoprefixer: - specifier: ^10.4.19 - version: 10.4.23(postcss@8.5.6) + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)) electron: - specifier: ^28.2.0 - version: 28.3.3 + specifier: ^33.3.1 + version: 33.4.11 electron-builder: - specifier: ^24.9.1 - version: 24.13.3(electron-builder-squirrel-windows@24.13.3) + specifier: ^25.1.8 + version: 25.1.8(electron-builder-squirrel-windows@25.1.8) electron-vite: - specifier: ^2.2.0 - version: 2.3.0(vite@5.4.21(@types/node@18.19.130)) + specifier: ^2.3.0 + version: 2.3.0(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)) eslint: - specifier: ^8.56.0 + specifier: ^8.57.1 version: 8.57.1 eslint-plugin-react: - specifier: ^7.33.2 + specifier: ^7.37.3 version: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@8.57.1) + specifier: ^5.1.0 + version: 5.2.0(eslint@8.57.1) husky: - specifier: ^9.0.11 + specifier: ^9.1.7 version: 9.1.7 postcss: - specifier: ^8.4.38 + specifier: ^8.4.49 version: 8.5.6 prettier: - specifier: ^3.2.4 + specifier: ^3.4.2 version: 3.8.0 react: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1 react-dom: - specifier: ^18.2.0 + specifier: ^18.3.1 version: 18.3.1(react@18.3.1) tailwindcss: - specifier: ^3.4.3 - version: 3.4.19 + specifier: ^4.0.0 + version: 4.1.18 typescript: - specifier: ^5.3.3 + specifier: ^5.7.2 version: 5.9.3 vite: - specifier: ^5.0.12 - version: 5.4.21(@types/node@18.19.130) - vite-plugin-native: - specifier: ^0.2.0 - version: 0.2.0 + specifier: ^6.0.7 + version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2) packages: 7zip-bin@5.2.0: resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - '@babel/code-frame@7.28.6': resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} @@ -399,6 +392,9 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -409,10 +405,10 @@ packages: eslint: '>= 8.0.0' prettier: '>= 3.0.0' - '@electron-toolkit/eslint-config-ts@1.0.1': - resolution: {integrity: sha512-6RkoGE/W/WVlDozy/u6/lo5THNP5CCAPxdKpdQuYaopUfObs3o+JyxdlSb45imh+nL8nv875970EoyplntXaug==} + '@electron-toolkit/eslint-config-ts@3.1.0': + resolution: {integrity: sha512-MowZQKd3yxXSDLack5QvjQwYHhpOJFoWBGBwJ/k+DCd7NUSendplECbQGFp86tPQYPUrPBPceR/hdsSAnaY5ZQ==} peerDependencies: - eslint: '>=8.0.0' + eslint: '>=9.0.0' typescript: '*' peerDependenciesMeta: typescript: @@ -448,23 +444,28 @@ packages: engines: {node: '>=12.13.0'} hasBin: true - '@electron/notarize@2.2.1': - resolution: {integrity: sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==} + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} engines: {node: '>= 10.0.0'} - '@electron/osx-sign@1.0.5': - resolution: {integrity: sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==} + '@electron/osx-sign@1.3.1': + resolution: {integrity: sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==} engines: {node: '>=12.0.0'} hasBin: true + '@electron/rebuild@3.6.1': + resolution: {integrity: sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==} + engines: {node: '>=12.13.0'} + hasBin: true + '@electron/rebuild@3.7.2': resolution: {integrity: sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==} engines: {node: '>=12.13.0'} hasBin: true - '@electron/universal@1.5.1': - resolution: {integrity: sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==} - engines: {node: '>=8.6'} + '@electron/universal@2.0.1': + resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} + engines: {node: '>=16.4'} '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} @@ -472,138 +473,294 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -622,6 +779,10 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -658,6 +819,14 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -685,10 +854,6 @@ packages: '@tanstack/query-core': '>= 4.0.0' '@tanstack/react-query': '>= 4.0.0' - '@malept/cross-spawn-promise@1.1.1': - resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} - engines: {node: '>= 10'} - '@malept/cross-spawn-promise@2.0.0': resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} engines: {node: '>= 12.13.0'} @@ -1098,6 +1263,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -1189,10 +1367,6 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} - '@remix-run/router@1.23.2': - resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} - engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1329,11 +1503,101 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + '@tailwindcss/typography@0.5.19': resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@tanstack/eslint-plugin-query@5.91.3': resolution: {integrity: sha512-5GMGZMYFK9dOvjpdedjJs4hU40EdPuO2AjzObQzP7eOSsikunCfrXaU3oNGXSsvoU9ve1Z1xQZZuDyPi0C1M7Q==} peerDependencies: @@ -1557,9 +1821,6 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -1575,8 +1836,11 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + + '@types/node@22.19.7': + resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} '@types/plist@3.0.5': resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} @@ -1595,9 +1859,6 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -1607,26 +1868,20 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/eslint-plugin@8.53.0': + resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.53.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/parser@8.53.0': + resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/project-service@8.53.0': resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} @@ -1634,10 +1889,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@8.53.0': resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1648,45 +1899,23 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/type-utils@8.53.0': + resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/types@8.53.0': resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@8.53.0': resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - '@typescript-eslint/utils@8.53.0': resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1694,10 +1923,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@8.53.0': resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1736,6 +1961,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -1771,22 +2000,18 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - app-builder-bin@4.0.0: - resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} + app-builder-bin@5.0.0-alpha.10: + resolution: {integrity: sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==} - app-builder-lib@24.13.3: - resolution: {integrity: sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==} + app-builder-lib@25.1.8: + resolution: {integrity: sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==} engines: {node: '>=14.0.0'} peerDependencies: - dmg-builder: 24.13.3 - electron-builder-squirrel-windows: 24.13.3 + dmg-builder: 25.1.8 + electron-builder-squirrel-windows: 25.1.8 + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} @@ -1800,8 +2025,10 @@ packages: resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} engines: {node: '>= 10'} - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1821,10 +2048,6 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -1871,13 +2094,6 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - autoprefixer@10.4.23: - resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1892,12 +2108,8 @@ packages: resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} hasBin: true - better-sqlite3@9.6.0: - resolution: {integrity: sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1921,10 +2133,6 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1933,26 +2141,22 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-equal@1.0.1: - resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} - engines: {node: '>=0.4'} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - builder-util-runtime@9.2.4: - resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==} + builder-util-runtime@9.2.10: + resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==} engines: {node: '>=12.0.0'} builder-util-runtime@9.5.1: resolution: {integrity: sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==} engines: {node: '>=12.0.0'} - builder-util@24.13.1: - resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==} + builder-util@25.1.7: + resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==} cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} @@ -1986,10 +2190,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - caniuse-lite@1.0.30001764: resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} @@ -2001,10 +2201,6 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -2066,14 +2262,14 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} @@ -2092,8 +2288,11 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - config-file-ts@0.2.6: - resolution: {integrity: sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==} + config-file-ts@0.2.8-rc1: + resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} @@ -2111,6 +2310,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -2177,8 +2380,11 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-fns@3.6.0: - resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} @@ -2222,6 +2428,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2232,21 +2441,11 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - dir-compare@3.3.0: - resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - dmg-builder@24.13.3: - resolution: {integrity: sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==} + dmg-builder@25.1.8: + resolution: {integrity: sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==} dmg-license@1.0.11: resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} @@ -2266,12 +2465,13 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} - dotenv-expand@5.1.0: - resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} - dotenv@9.0.2: - resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} - engines: {node: '>=10'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -2285,11 +2485,11 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-builder-squirrel-windows@24.13.3: - resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==} + electron-builder-squirrel-windows@25.1.8: + resolution: {integrity: sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==} - electron-builder@24.13.3: - resolution: {integrity: sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==} + electron-builder@25.1.8: + resolution: {integrity: sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==} engines: {node: '>=14.0.0'} hasBin: true @@ -2297,8 +2497,8 @@ packages: resolution: {integrity: sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==} engines: {node: '>= 14'} - electron-publish@24.13.1: - resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} + electron-publish@25.1.7: + resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==} electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -2317,8 +2517,8 @@ packages: '@swc/core': optional: true - electron@28.3.3: - resolution: {integrity: sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==} + electron@33.4.11: + resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} engines: {node: '>= 12.20.55'} hasBin: true @@ -2334,6 +2534,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2388,6 +2592,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2416,11 +2625,11 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2488,10 +2697,6 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2526,10 +2731,6 @@ packages: filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2557,9 +2758,6 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2567,6 +2765,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} @@ -2597,6 +2799,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -2637,10 +2844,6 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2670,14 +2873,14 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - goober@2.1.18: resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} peerDependencies: @@ -2725,6 +2928,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2740,6 +2946,10 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http2-wrapper@1.0.3: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} @@ -2748,6 +2958,10 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -2772,6 +2986,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2827,10 +3045,6 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -2894,10 +3108,6 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -2979,10 +3189,6 @@ packages: engines: {node: '>=10'} hasBin: true - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -3054,9 +3260,75 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3147,10 +3419,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@0.368.0: - resolution: {integrity: sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==} + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3178,14 +3450,6 @@ packages: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3211,6 +3475,10 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3218,10 +3486,6 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3276,9 +3540,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3307,6 +3568,11 @@ packages: node-api-version@0.2.1: resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -3323,14 +3589,15 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3435,9 +3702,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -3445,22 +3712,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -3469,53 +3724,10 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -3644,11 +3856,11 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-day-picker@8.10.1: - resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + react-day-picker@9.13.0: + resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==} + engines: {node: '>=18'} peerDependencies: - date-fns: ^2.28.0 || ^3.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: '>=16.8.0' react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} @@ -3695,18 +3907,22 @@ packages: '@types/react': optional: true - react-router-dom@6.30.3: - resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} - engines: {node: '>=14.0.0'} + react-router-dom@7.12.0: + resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react: '>=18' + react-dom: '>=18' - react-router@6.30.3: - resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==} - engines: {node: '>=14.0.0'} + react-router@7.12.0: + resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} @@ -3726,13 +3942,6 @@ packages: resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} hasBin: true - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - read-config-file@6.3.2: - resolution: {integrity: sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==} - engines: {node: '>=12.0.0'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -3743,10 +3952,6 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3763,6 +3968,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -3774,11 +3983,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true @@ -3865,6 +4069,12 @@ packages: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3918,10 +4128,6 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -4017,11 +4223,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -4041,15 +4242,12 @@ packages: tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} - tailwindcss-animate@1.0.7: - resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} - tailwindcss@3.4.19: - resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} - engines: {node: '>=14.0.0'} - hasBin: true + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -4072,13 +4270,6 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4103,28 +4294,15 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4159,6 +4337,13 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript-eslint@8.53.0: + resolution: {integrity: sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -4176,8 +4361,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} @@ -4243,25 +4428,27 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} - vite-plugin-native@0.2.0: - resolution: {integrity: sha512-7L/PdGqMkf2/k50svlf+elVTXl50wcoNTOJDms2mHkLngLoA2p0ejC10LAMgSETFBnlSQUrU0PBhLVf3BuYL3g==} - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' less: '*' lightningcss: ^1.21.0 sass: '*' sass-embedded: '*' stylus: '*' sugarss: '*' - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -4276,6 +4463,10 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -4304,6 +4495,9 @@ packages: engines: {node: '>= 8'} hasBin: true + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4366,8 +4560,6 @@ snapshots: 7zip-bin@5.2.0: {} - '@alloc/quick-lru@5.2.0': {} - '@babel/code-frame@7.28.6': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4485,11 +4677,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@commitlint/cli@19.8.1(@types/node@18.19.130)(typescript@5.9.3)': + '@commitlint/cli@19.8.1(@types/node@22.19.7)(typescript@5.9.3)': dependencies: '@commitlint/format': 19.8.1 '@commitlint/lint': 19.8.1 - '@commitlint/load': 19.8.1(@types/node@18.19.130)(typescript@5.9.3) + '@commitlint/load': 19.8.1(@types/node@22.19.7)(typescript@5.9.3) '@commitlint/read': 19.8.1 '@commitlint/types': 19.8.1 tinyexec: 1.0.2 @@ -4536,7 +4728,7 @@ snapshots: '@commitlint/rules': 19.8.1 '@commitlint/types': 19.8.1 - '@commitlint/load@19.8.1(@types/node@18.19.130)(typescript@5.9.3)': + '@commitlint/load@19.8.1(@types/node@22.19.7)(typescript@5.9.3)': dependencies: '@commitlint/config-validator': 19.8.1 '@commitlint/execute-rule': 19.8.1 @@ -4544,7 +4736,7 @@ snapshots: '@commitlint/types': 19.8.1 chalk: 5.6.2 cosmiconfig: 9.0.0(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@18.19.130)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@22.19.7)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -4595,6 +4787,8 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@date-fns/tz@1.4.1': {} + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.12.6 @@ -4609,27 +4803,28 @@ snapshots: transitivePeerDependencies: - '@types/eslint' - '@electron-toolkit/eslint-config-ts@1.0.1(eslint@8.57.1)(typescript@5.9.3)': + '@electron-toolkit/eslint-config-ts@3.1.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@eslint/js': 9.39.2 eslint: 8.57.1 + globals: 16.5.0 + typescript-eslint: 8.53.0(eslint@8.57.1)(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@electron-toolkit/preload@3.0.2(electron@28.3.3)': + '@electron-toolkit/preload@3.0.2(electron@33.4.11)': dependencies: - electron: 28.3.3 + electron: 33.4.11 - '@electron-toolkit/tsconfig@1.0.1(@types/node@18.19.130)': + '@electron-toolkit/tsconfig@1.0.1(@types/node@22.19.7)': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 - '@electron-toolkit/utils@3.0.0(electron@28.3.3)': + '@electron-toolkit/utils@3.0.0(electron@33.4.11)': dependencies: - electron: 28.3.3 + electron: 33.4.11 '@electron/asar@3.4.1': dependencies: @@ -4667,7 +4862,7 @@ snapshots: - bluebird - supports-color - '@electron/notarize@2.2.1': + '@electron/notarize@2.5.0': dependencies: debug: 4.4.3 fs-extra: 9.1.0 @@ -4675,7 +4870,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@electron/osx-sign@1.0.5': + '@electron/osx-sign@1.3.1': dependencies: compare-version: 0.1.2 debug: 4.4.3 @@ -4686,6 +4881,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/rebuild@3.6.1': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.3 + detect-libc: 2.1.2 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.86.0 + node-api-version: 0.2.1 + node-gyp: 9.4.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.3 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + '@electron/rebuild@3.7.2': dependencies: '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 @@ -4706,14 +4921,14 @@ snapshots: - bluebird - supports-color - '@electron/universal@1.5.1': + '@electron/universal@2.0.1': dependencies: '@electron/asar': 3.4.1 - '@malept/cross-spawn-promise': 1.1.1 + '@malept/cross-spawn-promise': 2.0.0 debug: 4.4.3 - dir-compare: 3.3.0 - fs-extra: 9.1.0 - minimatch: 3.1.2 + dir-compare: 4.2.0 + fs-extra: 11.3.3 + minimatch: 9.0.5 plist: 3.1.0 transitivePeerDependencies: - supports-color @@ -4721,72 +4936,150 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -4810,6 +5103,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@eslint/js@9.39.2': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -4845,6 +5140,12 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4878,10 +5179,6 @@ snapshots: '@tanstack/query-core': 5.90.19 '@tanstack/react-query': 5.90.19(react@18.3.1) - '@malept/cross-spawn-promise@1.1.1': - dependencies: - cross-spawn: 7.0.6 - '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.6 @@ -5309,6 +5606,26 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: react: 18.3.1 @@ -5376,8 +5693,6 @@ snapshots: '@remirror/core-constants@3.0.0': {} - '@remix-run/router@1.23.2': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.55.1': @@ -5461,10 +5776,78 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19)': + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.19 + tailwindcss: 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2) '@tanstack/eslint-plugin-query@5.91.3(eslint@8.57.1)(typescript@5.9.3)': dependencies: @@ -5694,18 +6077,18 @@ snapshots: '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 18.19.130 + '@types/node': 22.19.7 '@types/responselike': 1.0.3 '@types/conventional-commits-parser@5.0.2': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 '@types/debug@4.1.12': dependencies: @@ -5715,15 +6098,13 @@ snapshots: '@types/fs-extra@9.0.13': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 '@types/http-cache-semantics@4.0.4': {} - '@types/json-schema@7.0.15': {} - '@types/keyv@3.1.4': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 '@types/linkify-it@5.0.0': {} @@ -5736,13 +6117,17 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@18.19.130': + '@types/node@20.19.30': dependencies: - undici-types: 5.26.5 + undici-types: 6.21.0 + + '@types/node@22.19.7': + dependencies: + undici-types: 6.21.0 '@types/plist@3.0.5': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 xmlbuilder: 15.1.1 optional: true @@ -5759,9 +6144,7 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 18.19.130 - - '@types/semver@7.7.1': {} + '@types/node': 22.19.7 '@types/use-sync-external-store@0.0.6': {} @@ -5770,38 +6153,33 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 optional: true - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 + '@typescript-eslint/parser': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/type-utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.0 eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 + ignore: 7.0.5 natural-compare: 1.4.0 - semver: 7.7.3 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 eslint: 8.57.1 - optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5815,11 +6193,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/scope-manager@8.53.0': dependencies: '@typescript-eslint/types': 8.53.0 @@ -5829,37 +6202,20 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.53.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) debug: 4.4.3 eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/types@8.53.0': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.7.3 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) @@ -5875,20 +6231,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.1 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - eslint: 8.57.1 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - typescript - '@typescript-eslint/utils@8.53.0(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -5900,11 +6242,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.53.0': dependencies: '@typescript-eslint/types': 8.53.0 @@ -5912,7 +6249,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@18.19.130))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -5920,7 +6257,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21(@types/node@18.19.130) + vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -5945,6 +6282,8 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -5982,49 +6321,50 @@ snapshots: ansi-styles@6.2.3: {} - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - app-builder-bin@4.0.0: {} + app-builder-bin@5.0.0-alpha.10: {} - app-builder-lib@24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3): + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8): dependencies: '@develar/schema-utils': 2.6.5 - '@electron/notarize': 2.2.1 - '@electron/osx-sign': 1.0.5 - '@electron/universal': 1.5.1 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.1 + '@electron/rebuild': 3.6.1 + '@electron/universal': 2.0.1 '@malept/flatpak-bundler': 0.4.0 '@types/fs-extra': 9.0.13 async-exit-hook: 2.0.1 bluebird-lst: 1.0.9 - builder-util: 24.13.1 - builder-util-runtime: 9.2.4 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 chromium-pickle-js: 0.2.0 + config-file-ts: 0.2.8-rc1 debug: 4.4.3 - dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 ejs: 3.1.10 - electron-builder-squirrel-windows: 24.13.3(dmg-builder@24.13.3) - electron-publish: 24.13.1 + electron-builder-squirrel-windows: 25.1.8(dmg-builder@25.1.8) + electron-publish: 25.1.7 form-data: 4.0.5 fs-extra: 10.1.0 hosted-git-info: 4.1.0 is-ci: 3.0.1 isbinaryfile: 5.0.7 js-yaml: 4.1.1 + json5: 2.2.3 lazy-val: 1.0.5 - minimatch: 5.1.6 - read-config-file: 6.3.2 + minimatch: 10.1.1 + resedit: 1.7.2 sanitize-filename: 1.6.3 semver: 7.7.3 tar: 6.2.1 temp-file: 3.4.0 transitivePeerDependencies: + - bluebird - supports-color + aproba@2.1.0: {} + archiver-utils@2.1.0: dependencies: glob: 7.2.3 @@ -6061,7 +6401,10 @@ snapshots: tar-stream: 2.2.0 zip-stream: 4.1.1 - arg@5.0.2: {} + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 argparse@2.0.1: {} @@ -6087,8 +6430,6 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 - array-union@2.1.0: {} - array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.8 @@ -6146,15 +6487,6 @@ snapshots: at-least-node@1.0.0: {} - autoprefixer@10.4.23(postcss@8.5.6): - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001764 - fraction.js: 5.3.4 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -6165,13 +6497,11 @@ snapshots: baseline-browser-mapping@2.9.15: {} - better-sqlite3@9.6.0: + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 - binary-extensions@2.3.0: {} - bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -6200,10 +6530,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.15 @@ -6214,8 +6540,6 @@ snapshots: buffer-crc32@0.2.13: {} - buffer-equal@1.0.1: {} - buffer-from@1.1.2: {} buffer@5.7.1: @@ -6223,7 +6547,7 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - builder-util-runtime@9.2.4: + builder-util-runtime@9.2.10: dependencies: debug: 4.4.3 sax: 1.4.4 @@ -6237,19 +6561,19 @@ snapshots: transitivePeerDependencies: - supports-color - builder-util@24.13.1: + builder-util@25.1.7: dependencies: 7zip-bin: 5.2.0 '@types/debug': 4.1.12 - app-builder-bin: 4.0.0 + app-builder-bin: 5.0.0-alpha.10 bluebird-lst: 1.0.9 - builder-util-runtime: 9.2.4 + builder-util-runtime: 9.2.10 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 fs-extra: 10.1.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 is-ci: 3.0.1 js-yaml: 4.1.1 source-map-support: 0.5.21 @@ -6314,8 +6638,6 @@ snapshots: callsites@3.1.0: {} - camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001764: {} chalk@4.1.2: @@ -6325,18 +6647,6 @@ snapshots: chalk@5.6.2: {} - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - chownr@1.1.4: {} chownr@2.0.0: {} @@ -6395,12 +6705,12 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - commander@4.1.1: {} - commander@5.1.0: {} compare-func@2.0.0: @@ -6419,11 +6729,13 @@ snapshots: concat-map@0.0.1: {} - config-file-ts@0.2.6: + config-file-ts@0.2.8-rc1: dependencies: glob: 10.5.0 typescript: 5.9.3 + console-control-strings@1.1.0: {} + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -6441,14 +6753,16 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + core-util-is@1.0.2: optional: true core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@18.19.130)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + cosmiconfig-typescript-loader@6.2.0(@types/node@22.19.7)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): dependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 2.6.1 typescript: 5.9.3 @@ -6506,7 +6820,9 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns@3.6.0: {} + date-fns-jalali@4.1.0-0: {} + + date-fns@4.1.0: {} dayjs@1.11.19: {} @@ -6542,6 +6858,8 @@ snapshots: delayed-stream@1.0.0: {} + delegates@1.0.0: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -6549,30 +6867,23 @@ snapshots: detect-node@2.1.0: optional: true - didyoumean@1.2.2: {} - - dir-compare@3.3.0: + dir-compare@4.2.0: dependencies: - buffer-equal: 1.0.1 minimatch: 3.1.2 + p-limit: 3.1.0 - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - dlv@1.1.3: {} - - dmg-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): + dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3) - builder-util: 24.13.1 - builder-util-runtime: 9.2.4 + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 fs-extra: 10.1.0 iconv-lite: 0.6.3 js-yaml: 4.1.1 optionalDependencies: dmg-license: 1.0.11 transitivePeerDependencies: + - bluebird - electron-builder-squirrel-windows - supports-color @@ -6600,9 +6911,11 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv-expand@5.1.0: {} + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 - dotenv@9.0.2: {} + dotenv@16.6.1: {} dunder-proto@1.0.1: dependencies: @@ -6616,40 +6929,41 @@ snapshots: dependencies: jake: 10.9.4 - electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3): + electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): dependencies: - app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) archiver: 5.3.2 - builder-util: 24.13.1 + builder-util: 25.1.7 fs-extra: 10.1.0 transitivePeerDependencies: + - bluebird - dmg-builder - supports-color - electron-builder@24.13.3(electron-builder-squirrel-windows@24.13.3): + electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 24.13.3(dmg-builder@24.13.3)(electron-builder-squirrel-windows@24.13.3) - builder-util: 24.13.1 - builder-util-runtime: 9.2.4 + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 chalk: 4.1.2 - dmg-builder: 24.13.3(electron-builder-squirrel-windows@24.13.3) + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) fs-extra: 10.1.0 is-ci: 3.0.1 lazy-val: 1.0.5 - read-config-file: 6.3.2 simple-update-notifier: 2.0.0 yargs: 17.7.2 transitivePeerDependencies: + - bluebird - electron-builder-squirrel-windows - supports-color electron-log@5.4.3: {} - electron-publish@24.13.1: + electron-publish@25.1.7: dependencies: '@types/fs-extra': 9.0.13 - builder-util: 24.13.1 - builder-util-runtime: 9.2.4 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 chalk: 4.1.2 fs-extra: 10.1.0 lazy-val: 1.0.5 @@ -6672,7 +6986,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@2.3.0(vite@5.4.21(@types/node@18.19.130)): + electron-vite@2.3.0(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)): dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.6) @@ -6680,14 +6994,14 @@ snapshots: esbuild: 0.21.5 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 5.4.21(@types/node@18.19.130) + vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color - electron@28.3.3: + electron@33.4.11: dependencies: '@electron/get': 2.0.3 - '@types/node': 18.19.130 + '@types/node': 20.19.30 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color @@ -6705,6 +7019,11 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + entities@4.5.0: {} env-paths@2.2.1: {} @@ -6845,6 +7164,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -6862,7 +7210,7 @@ snapshots: optionalDependencies: eslint-config-prettier: 9.1.2(eslint@8.57.1) - eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -6979,14 +7327,6 @@ snapshots: fast-diff@1.3.0: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -7015,10 +7355,6 @@ snapshots: dependencies: minimatch: 5.1.6 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7055,8 +7391,6 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - fraction.js@5.3.4: {} - fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -7065,6 +7399,12 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 @@ -7100,6 +7440,17 @@ snapshots: functions-have-names@1.2.3: {} + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -7144,10 +7495,6 @@ snapshots: github-from-package@0.0.0: {} - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -7196,20 +7543,13 @@ snapshots: dependencies: type-fest: 0.20.2 + globals@16.5.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 gopd: 1.2.0 - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - goober@2.1.18(csstype@3.2.3): dependencies: csstype: 3.2.3 @@ -7261,6 +7601,8 @@ snapshots: dependencies: has-symbols: 1.1.0 + has-unicode@2.0.1: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -7279,6 +7621,13 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http2-wrapper@1.0.3: dependencies: quick-lru: 5.1.1 @@ -7291,6 +7640,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -7311,6 +7667,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7363,10 +7721,6 @@ snapshots: dependencies: has-bigints: 1.1.0 - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -7426,8 +7780,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-number@7.0.0: {} - is-obj@2.0.0: {} is-path-inside@3.0.3: {} @@ -7508,8 +7860,6 @@ snapshots: filelist: 1.0.4 picocolors: 1.1.1 - jiti@1.21.7: {} - jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -7571,7 +7921,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lilconfig@3.1.3: {} + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 lines-and-columns@1.2.4: {} @@ -7642,7 +8039,7 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@0.368.0(react@18.3.1): + lucide-react@0.469.0(react@18.3.1): dependencies: react: 18.3.1 @@ -7692,13 +8089,6 @@ snapshots: meow@12.1.1: {} - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - mime-db@1.52.0: {} mime-types@2.1.35: @@ -7713,6 +8103,10 @@ snapshots: mimic-response@3.1.0: {} + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -7721,10 +8115,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.2 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -7774,12 +8164,6 @@ snapshots: ms@2.1.3: {} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - nanoid@3.3.11: {} napi-build-utils@2.0.0: {} @@ -7801,6 +8185,23 @@ snapshots: dependencies: semver: 7.7.3 + node-gyp@9.4.1: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.3 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + node-releases@2.0.27: {} nopt@6.0.0: @@ -7811,9 +8212,14 @@ snapshots: normalize-url@6.1.0: {} - object-assign@4.1.1: {} + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 - object-hash@3.0.0: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -7936,20 +8342,14 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-type@4.0.0: {} + pe-library@0.4.1: {} pend@1.2.0: {} picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.3: {} - pify@2.3.0: {} - - pirates@4.0.7: {} - plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.11 @@ -7958,42 +8358,11 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.11 - - postcss-js@4.1.0(postcss@8.5.6): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.6 - - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.6 - - postcss-nested@6.2.0(postcss@8.5.6): - dependencies: - postcss: 8.5.6 - postcss-selector-parser: 6.1.2 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -8165,9 +8534,11 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): + react-day-picker@9.13.0(react@18.3.1): dependencies: - date-fns: 3.6.0 + '@date-fns/tz': 1.4.1 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 react: 18.3.1 react-dom@18.3.1(react@18.3.1): @@ -8210,17 +8581,19 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 - react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router-dom@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@remix-run/router': 1.23.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router: 6.30.3(react@18.3.1) + react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-router@6.30.3(react@18.3.1): + react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@remix-run/router': 1.23.2 + cookie: 1.1.1 react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: @@ -8240,19 +8613,6 @@ snapshots: transitivePeerDependencies: - supports-color - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - read-config-file@6.3.2: - dependencies: - config-file-ts: 0.2.6 - dotenv: 9.0.2 - dotenv-expand: 5.1.0 - js-yaml: 4.1.1 - json5: 2.2.3 - lazy-val: 1.0.5 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -8273,10 +8633,6 @@ snapshots: dependencies: minimatch: 5.1.6 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -8301,18 +8657,16 @@ snapshots: require-from-string@2.0.2: {} + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} resolve-from@5.0.0: {} - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.5: dependencies: is-core-module: 2.16.1 @@ -8430,6 +8784,10 @@ snapshots: type-fest: 0.13.1 optional: true + set-blocking@2.0.0: {} + + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8502,8 +8860,6 @@ snapshots: dependencies: semver: 7.7.3 - slash@3.0.0: {} - slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -8627,16 +8983,6 @@ snapshots: strip-json-comments@3.1.1: {} - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.15 - ts-interface-checker: 0.1.13 - sumchecker@3.0.1: dependencies: debug: 4.4.3 @@ -8655,37 +9001,9 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.19): - dependencies: - tailwindcss: 3.4.19 + tailwindcss@4.1.18: {} - tailwindcss@3.4.19: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) - postcss-nested: 6.2.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - resolve: 1.22.11 - sucrase: 3.35.1 - transitivePeerDependencies: - - tsx - - yaml + tapable@2.3.0: {} tar-fs@2.1.4: dependencies: @@ -8720,14 +9038,6 @@ snapshots: text-table@0.2.0: {} - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - through@2.3.8: {} tiny-typed-emitter@2.1.0: {} @@ -8749,24 +9059,14 @@ snapshots: tmp@0.2.5: {} - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.5 - ts-api-utils@1.4.3(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-interface-checker@0.1.13: {} - tslib@2.8.1: {} tunnel-agent@0.6.0: @@ -8815,6 +9115,17 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript-eslint@8.53.0(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -8829,7 +9140,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@5.26.5: {} + undici-types@6.21.0: {} unicorn-magic@0.1.0: {} @@ -8885,16 +9196,19 @@ snapshots: extsprintf: 1.4.1 optional: true - vite-plugin-native@0.2.0: {} - - vite@5.4.21(@types/node@18.19.130): + vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: - esbuild: 0.21.5 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.55.1 + tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.7 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 w3c-keyname@2.2.8: {} @@ -8947,6 +9261,10 @@ snapshots: dependencies: isexe: 2.0.0 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..305296e --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +ignoredBuiltDependencies: + - better-sqlite3 + - electron + - esbuild diff --git a/postcss.config.js b/postcss.config.js index 85f717c..4080114 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,3 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } + plugins: {} } diff --git a/src/main/api.ts b/src/main/api.ts index a6b42e4..efc5aab 100644 --- a/src/main/api.ts +++ b/src/main/api.ts @@ -45,6 +45,7 @@ import { getTemplateTags } from './repository/surgery-template' import { createBackup, listBackups, deleteBackup, restoreBackup } from './backup' +import { getDashboardStats, getRecentActivity } from './repository/dashboard' // Custom APIs for renderer export const api = { @@ -91,7 +92,10 @@ export const api = { createBackup, listBackups, deleteBackup, - restoreBackup + restoreBackup, + + getDashboardStats, + getRecentActivity } export type ApiType = typeof api diff --git a/src/main/repository/dashboard.ts b/src/main/repository/dashboard.ts new file mode 100644 index 0000000..9b02a4f --- /dev/null +++ b/src/main/repository/dashboard.ts @@ -0,0 +1,115 @@ +import { db } from '../db' +import { sql } from 'kysely' + +export interface DashboardStats { + totalPatients: number + totalSurgeries: number + totalDoctors: number + surgeriesThisMonth: number + patientsThisMonth: number +} + +export interface RecentActivityItem { + id: number + type: 'patient' | 'surgery' + title: string + subtitle: string + date: string +} + +export async function getDashboardStats(): Promise { + const now = new Date() + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1) + const monthStartTimestamp = firstDayOfMonth.getTime() + + // Get total counts + const patientCount = await db + .selectFrom('patients') + .select(sql`count(*)`.as('count')) + .executeTakeFirst() + + const surgeryCount = await db + .selectFrom('surgeries') + .select(sql`count(*)`.as('count')) + .executeTakeFirst() + + const doctorCount = await db + .selectFrom('doctors') + .select(sql`count(*)`.as('count')) + .executeTakeFirst() + + // Get this month's counts using raw SQL for date comparison + const surgeriesThisMonth = await db + .selectFrom('surgeries') + .select(sql`count(*)`.as('count')) + .where(sql`created_at >= ${monthStartTimestamp}`) + .executeTakeFirst() + + const patientsThisMonth = await db + .selectFrom('patients') + .select(sql`count(*)`.as('count')) + .where(sql`created_at >= ${monthStartTimestamp}`) + .executeTakeFirst() + + return { + totalPatients: Number(patientCount?.count || 0), + totalSurgeries: Number(surgeryCount?.count || 0), + totalDoctors: Number(doctorCount?.count || 0), + surgeriesThisMonth: Number(surgeriesThisMonth?.count || 0), + patientsThisMonth: Number(patientsThisMonth?.count || 0) + } +} + +export async function getRecentActivity(limit: number = 10): Promise { + // Get recent patients + const recentPatients = await db + .selectFrom('patients') + .select(['id', 'name', 'phn', 'created_at']) + .orderBy('created_at', 'desc') + .limit(limit) + .execute() + + // Get recent surgeries with patient info + const recentSurgeries = await db + .selectFrom('surgeries') + .innerJoin('patients', 'patients.id', 'surgeries.patient_id') + .select([ + 'surgeries.id', + 'surgeries.bht', + 'surgeries.title', + 'surgeries.created_at', + 'patients.name' + ]) + .orderBy('surgeries.created_at', 'desc') + .limit(limit) + .execute() + + // Helper to convert Date to ISO string + const dateToString = (date: Date | null | undefined): string => { + if (!date) return '' + return date instanceof Date ? date.toISOString() : String(date) + } + + // Combine and sort by date + const activity: RecentActivityItem[] = [ + ...recentPatients.map((p) => ({ + id: p.id, + type: 'patient' as const, + title: p.name, + subtitle: `PHN: ${p.phn}`, + date: dateToString(p.created_at) + })), + ...recentSurgeries.map((s) => ({ + id: s.id, + type: 'surgery' as const, + title: s.name, + subtitle: s.title || `BHT: ${s.bht}`, + date: dateToString(s.created_at) + })) + ] + + // Sort by date descending and limit + return activity + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, limit) +} diff --git a/src/renderer/src/assets/opnotes-logo.png b/src/renderer/src/assets/opnotes-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1c2d17200b60bd1ab6a53319a6619842bea031a2 GIT binary patch literal 30394 zcmdSAWmH>T)GeF@cQ5W*yhsU7i(7Fk?iJjfP~6=qQrxw;1aF~)LW{cp}J z{@;I>F%HQ{a`s+(oh5V6wRWWXJ9!*T3QQ0Pgrle+qX_~*fVU73Ix6sT;q~_pc)@T| zFmMHdaQpwez{8GZR=|hkZnFArT8=hup5`vrAWu(E?vDthpUsY_pHVC_o@O zkfO{RZLgezCC?1v!Rg^s$8kH0Am(jO13vU5es)ysh@eUfc+Mq-2?>d8=Ra(lK`)${ zs(d;*d;$#@TnM{xqg_tG^Nt|h$C3^kpT6UBv2z%&j=z;{yPBTfR|vuX{hu)U=rKq} z)5tnsM7?x#W?m{b$#fKHPRizIt$|c|&m)CeRL~)F&p47tdgtrM!|&i(^eV6}h(q~F zMaC~5S&HfPComUGH}L*aJqOE|eIYa%n-lymrPBsH3QLH0j${&N1~L3gMQTS958{?m z&i_(Nz?^Gj#(hG~OLFt#W^5)xCMkvxL~zWGBn~ryl0HsL{hoY-fTD1O$rjxnK)H-cVAyuJ!HjFX4nWZYyyJBI%}{rEE38;@z|ZeyY=)Py%k*cB_iTS zKobfLTJ!32s4`Dut=Ou2PGlAX0|Sg-`+@$+wCEygY@cRLa)6m>#DEMqY+ zBq94CNv=Pm$kh&bRrirIYdW(MBqC3dc@aJHu=z0m`;1z_OsA+qUc*!v1+RuPGriSm zRnp0qv#*LuEqIT!-BT&j^Imw1o*#>kkG^p;HF5VLGrlSTM|M!%sKMN{jLhTECBQTR z55;?Sp{1{xtInRoZY8SAnyBiLWcn$p%LB)5d1vKW>c(zWa8)kHLY+ZT8Ux0Z?Enu9 znh!v0NN?E6H`l6?rZ_(3upYgo&h%}L)iJ`BeLd@8Wsh}MA83Hf@xqeF6npfA1&=AR z;cHn|T}Kay5LhkWK8jh94V+*#3BF14GMLsWrqJ!^FED1UYNjp-VC-Rxs`+M7^-X;< z5-$1Jjp}=(a^Nl5zeroJ$B829P}8;o-rhJsqfh#s0#a!B0iUC;$#f*JN*~&rq!hUx z_%ws@Y3h=T*pwu+E-EUg*OjQ*aWMB4%Kd$9eP9N}(4Ur`4RnMmm;PYFME??y!ubU+ z_doe+b&N{+z#U;wrIwJwc!W2)^Uvl2Xo!xs9XA1}PI@+a+cNNx{3@Mj^?C|bG=5fP z)fE0#G8o@k8Bj{}b4`tsO)6i!r44tPOkD!ADRb_?E|>e^Iak4VRQpDe_# zHr32F+RLam=v;z6!%82Ztr4V3ASMG&GzytZ^lDKv91)Vrm5Br$_KI*Nk=Od6vVqkU zyj8r-cM${C(@jF?CWWSAjKE1u(TncN^O+=i}Sj7RG4$c zvF;o)kM;08%(Hsd0I8j7QuC>^9h!ZfTevm6v0fxU!tg&?V^Fxy7L}_-^q<>7`x(9O zdZ{BRIIN`KXt>EE!84)ILyiMyH8Y;LxxFr1XTqQdeQI?k&Hrv}>My@OKH@j-pqW15 zsK2AddZW4*&0JYVhG}{7Yq`0;RAngORHO7n{X!Kf4sCjP-Haho6GHdXeCXSVk@so5 zN+#9Z`hCMz9iokRf3uNJ{HYR^wK5|X6~@nSGKlfGmUtSKK#Q}$g_4r{Nok$ZLI`K zhijpC(HJ}uJ~JT}p-?0+{W5a2xf-rvyJXgnrN5bw8|OaYTxW^KnbmUKp*Y&NH&!O3%>|*P8-9xf4CRyF$=t+|CJZo<&C71?gpx+m;hMMqPX7o-o&WRGE|Q}NbpRk6-e zRHoEJik1%NhJoU1bO;bF1|)48Rt};4CE&b50!d^pbbbB$_w*oyQX-3&5;zN5xE4+z8g ztm?T!JB#6G3)~W{0iiqHPiD^4wYj2vF>Tf(od`8I&Nas8172ZnL zqRVVw`HLI=3Np4=MaWVQ_{e*ZRG2Y5*LKp8G!19I00T-?EOTXJpdSPpaI#p<3wQ8i z)6I^X)nbtjs_zPb!0A@`b0$EEWq(50Ba3O^zHkpS=6BXjU*X&8n!j!L_B3^QH;^dX z?}-bU&_&JO$hGrKsZ>pe6Zk#2myyp+6QOBP+~AivHfSY2YS7a~hSHtzI=w`@`-Kur zt4|d0t8OGkoLx6aalP^F>rK+Nzq66`)!7(JE*Mu21}wf`@!XJ_vktU3^DfY(EY^fq z-{Ph@&3kD5d;I%Dt8bfJfviZG)2erSN|%#XYJha$2(42lw9+CAh4dU5K?aVha0aC&Wjc%+vwf60?nRuBY3ceN#n)WHxBQN^pXJphAv8*(z>qXP!R4 z*(VZEJHgtXOZ`c=LthgAT9l$FU*Vm+;Z-mjK+>m<<+6B*_JH?;4-kwNNEiZ(UuLZR zZilAzlq>4t2nE+Pj5~VqU^sQW`FFwB@x{ze@#P4aCjhmm>|-t-w54OFcFHr9b1{zf zgd#ExkmbaKrs^`wX?TXcspu>KRPyR2nitA80gP3*@@ly-@vn`+?5+e0c+MIl5{}rlW)A@y7@z1182t`9)28GODa;Ewm}K#(L>2q|jgjCt zJLuK6!fHV0vZKL^=^x>BpJw<8Q_y=sY1C)cMR7Xye7c1;v&qB3hr$sha6+@V>mN*0 zFM$C^AbFszE)T1(9^BKb{GIYlXP!YI-KrM+C(24)@v0_)gUmlFZZkyN&Qdjz2qyRc z?xE?M`?Hv>dx#O^r(g{m6ss&!NT3DcA$oze?~QeNnbflN^OKr3FpyAs|+(<-<$*x-xv(El)i&50$5~+#rd{86bs^{?=#fk}ZSgRn#&UShD!6Y(GAj*gi zFHxdGE98Z4V*EIm6RT3%_F#NDymQIKXJkNeNRpQ*qll0hv@YjwMn%K%WS|!d-?Os& zL!5YGkajKrk2gkoPDqW_X~exs-{`vfi+Y0|3sDGZT2@+YCE7;xcX@-s ztNR_**I9PV~V_)I8-(AObnW?OaDIWIngd=M@IBa%c!+v?#O6{ z6M`aTC|C8>#Q#=AyWe@+d?R=TN1Oz*r?~(6sXzAK_ppTL_plT+i2-m3CWpQaOUC^@ zL;C)O3ot<;C)T8&1j&1Fm9m4A#pyo}qzQ#6bqL+sC)AvSVx-E?j#UorZxU}TO9y+| zq(-mw{ZV}mOIY7|J0u1JDPpZ1%#)Wcg}*;Z`Ae43LQ^aT0t&)3GFgAeEH& zoad#8tP}L7)6I<9pMh#}Kpo2PZ?K;Yr1RN~I(s#XziRq6FHMq5vOx?qeX= zBQ8=TBL+<1eOKKyE52l`U%?-DP1-*oH3<>NG0`)FT`pOipW zfp;KxqZWrns`+xdOr|<08@83;hnTHHUu|6Ih&nXM>ft#=xszhOxZeFNvt%9rRK7C? zLggAY!!71X_w(m-CO2XHBy0u7Uh_VEgym#Hpmq@0pxJJbe6hc$l+mCcEWHS;&R)8z z!z$A~tNf8C0HM2|ByAxLgzt%}lLji{I;)c>G`ZC#gO!nTJ;{?ciO}*KBBhr-9!00M zToZ2y(9S`iszUL=(u?Ahu7TlF!}UlMs*QH{F_GUTg>4^X{HzSqW(;oR^e-vRy%$vW z(R1W|83F;bdH(yt$+&f8&(qB)hgUWxGF=0rMzDcy>C=5x2t+hjUC27C=Us0Wy`h^bVS+W@Y(&AU9Qh`3Z%WB z{oUq*sdCXG+l&}lrU3nc!56*y;HoR*&Wn5k&}}jy=vMmWD(_4+-u@?U!bGUm;7>8o zKChb4=^@NcxLpe zN}4bmF@F|*Rb$xv!IBp{5&D9)5TvVoZK{_u?9ZSCLUx*dYH?d?FTXEgz+iqS2NDa| z&MJ%zLI&a=6xpdOZ2VIRvXo{pH4h^wT;Mb*#tNc}6UK<4gaL!4C%^elVS<$$!Gz6W zA_r1R4`rX~y2w`*mBE04e*dzUj`vN8W=d&gL)Grv@T?6ghcY<>3F3qgqOP}1ph*A1 zS%Z)ahN=V_pkxL?5L6(~JwXAoFa=26`bN{osj7`)Z+~fX1xR$fTEk6s+J2h58zcQo z784qzzb7f6ooEg~{F4NbzToHV7-^=X!BH_#rH?Al*nYuz;K&~|DRxM)@po+GPK3-0 zMX^(t>8kfckT)&W{6S}^g^9?-JhaxQ@%IZ6XC(CEOJ-~g7<3Y-AUdwG{ajG0YA8db zA_lBg5<|Pv98lW;KC2oCR5#+?9$9O}j;pAm1SSJ>3^!C#fVg-Ah$SPVnE((j1c}4|>IGO56hPwt2fs@B=Lt$G^4NZ>QLv&B zAtN6^5OOd}#UW1JpfvbTKS>kNlrNTz^Y^lS1_;n8>k)njPD;)=A zr4Lt`7Hy>m(t)Yo9d#-bFYr9G9(ySR!s(JkJnX`$7wUzoLWLy>3s2D$EwYg40OIBg zFlX2@hxAh0m(pJC1|@Sl^n?-^yhKhAkhlrqFUWD-PR+@iyBQYJ{jXK8ajk`FgJfpVrLR zWM6RWaHOrA<3&fW&FG*qEVS$ocm4D~e&;lGRYQd8bO>{}f9P;u@N95C^v-td)Xhxj4$Jjg$Y zq->$2O<-V1jLfLXqk~HF{4~ZA{;|6XAnpJ3eLwrC5pAZ$e<{LA>|@Wv$Ov_xycN+%`F<9 zSehrrn;##cz<`MgiJ|%%UoVZ8c}If>isPS1961r_iP@535c|tP9CseCCZ^n3LW%lWzE6Gzo@*4L8ZSvu?6+xU*$J9CL0!+~Sm*OV;ONMdHZOp!pKh~%QST`<){ z^%Re3D2{O;%h2A_tyNmu(=oO`TqyM4XOkiB*7-bh-gm#@?f$@&R`tGdz;@1 zIM$UXnCd#aA?DP2x z1BZ?sbmSllR}DqZ!SNQOMF1#u*!#)xFn<1$oMvs1Bfe7>v9OhbuyK+M+%w|{ESq_tNzfvC-})oaqx|2+yXsB> z9;7B-;;0)b3@HoO$E{JsuYB8R78XlIrI}oBcJ~U|=C|w9SYaCFM(=3Dx{F&uf_ zF~86A;jZ=LoA}fCSnt?1PMPO#&MV?rq0KhV)ju9nruqmt{J$7_jVLznix1j)nl%5~ zL0qbX4E2DS)(g5W)ES-+%`XdK3l}$fKNo0d|9D9`eLoKMk^03W_Gg>BsQ))s+u={r zA3SUVliOqQlF{}Ha(8@dcjl$9+{}9FmJRET43>SJQYtlqwUbkG<+i2wymtF8h5yG7 z>Z#Y8*Va^xgj9tOvjqR$rw{!uzI;M6Qc54OxUlZ`-bFn3^EEwxfo{ahy83*$$rVpa_6Oa74NlIEdI}o$Nslm=V{eULOgjCsT`A)(uVIOD;$5> zK1tq;7(Uz=)N6ET<+V9sP#X@6e@#`44!abt zSHrubt%<=QXH_Dp^ zvhI-vIi{e*MyY1d|BrK%zo^W;i;im;0-&0zqsq`83`y0%kbXf-tP=5`3IdV zkSlv%{#ynNmH)9HbXE?q($q+9^U>_j-`Ph+Wzai!y?KF;3ZruDm${f?xmeorVnG1B zQpBGTe_-mVSs}aJ&`%re(la5x+=z(i0l)+kEE6mJWYIToU{k-v)a$jp+iBpp!U&o4 zp&%l|?)|V(S(c3iwrE-N+=jWi#lW#aBd5uF4e_{v^e+r*@hL`m!2^MSh8A#*YWkPz zMdN`SOO7l5%^WegrII1CTz`-^*OH|pdral#lT)U}O%EhKlJEvnty#FBdsKYz68Ik27!8#BrJj7$e8 zLcy-5!@hNA<~z%Vf&!q99}R*%^=_B75A6X+9Uj~D_S@VZ+uO z`2&)Cuh3nKvo+x()>;3F7%9@WVc8lw{`6Y!i@A;VI_Nn*8L~A^Fje32geIAt zVPf8X7~+)3dKFz2c7^!RzfXGemzK>TtDzP%bgOsmSwmj_@GoWHW3>! zF!VQ6dDr_-AaqwU{(QfLDJqfzadak1rRZ$vx#jXi#KU(I7e3p%m#mp&)eoPl-kbm*T;gdOkNSh-zWpv zB?HN;o$JYK$d1PASxb5?L&$d5rFu8LHQiKyAjfq2hI%MKoYxsM4`I0eJ$v`Vt*IuU zO|hf%`QBKbt})S-k6y-1W$DM`54VwyDbN+yVE|y$z+T+=p1k+~iKDos+pzViz-M7_wm9qC zqLTcl<3UQ{9rBt&^+}n69VN%e1Y84t@3U*AXCg7Ya>=G<(XRD z6@`kkr&X%U#jaDOpj*zVWZhO#!|D9V(}c#m<9xQso4<0p%6#yQoy&mD*zI6+tr7_48z_Lf;0lN@odMAt%5;O0;vepc&p_mQ8XUSbBGE=xxsYnOR zcok+4;ru%pg}Y0IqN>>fLk(h&T8e%3FGoiEo9$n48I0&@m7J~JsAXRDV@Hh{vH747&o!*xV@brs>(brC@3N`jwgtaL@THf#>XPEklT?hRQdqdb0yU%bwR-`*B#W6 z)Ox{fJy|tdLsOyI#@Djs#yB45pR+9Pt6mb57)Yq-RPe1!sFz)<1Y5k_?cWzy#5apj zOhAk2i%Yqmw&bf!8^he}^=EVzeqyD&K8+rqo@{H)t=UJf1ir_@Ms-p3V1EKUDaT_? ztoELih8E>vY2=d9dpEk=?`*>kGdlXCej$4?A77sX?IcIvhC!T zyRuQ!t5nbfJr`M#<-JFa>yUc$@$AnkVOL9CCXPvDv-29GA^&S4z3$Uj*J=-GI@z(~ zAK6gfAvcZL5Kc0}HU$Z6hmN>PM4!xmVg~3pe5uU4=lPmV^L9mdYq9+YmrK$bwc8{C5dODfZ>;j~t^PE5`19$u+lYUkQp zD7`wIO5V<+kS>lQDfFRIv_de!73>z9GS7K>qIh~@kB*WBN4M3*ZO4bTy+dUWevI30 z#2INR-}YUSI|*dnq&KC2224cMN!jpHARb7DK|0XJ8`V*8H}bq}!Ex(Zt(Mx_Y4{N< zmnxE>3tb{B2ox$SYxHzX>Amqar7ZZI@i*EmL1_1%AQ34PI-(zFuaQ1SAcWPgq%ou} ztii(|Bv`JL<<;X(F?wmzN(@0M#5T> zo@`0#oq(?Qr)x{M;cqaE1$G>}A#uax-WT6kd2!2*1@ry&fhzwuQ6m1(56(@(daFhc zOW4=pg%HF>2o4+@?{O1ci$oF>V#0cT<~shn-5RT>KLOb-Uj($quGKCuMj^CbeJ#O@ zpIA%N-_|y}Ri78_e1#b?O_<{Xb#{OxecBo10c34OYR%p`t|3E?8_eXri46~ka$=eN zg%$_TEXz<2%jFB<;OUw@=4E=5sQ>~|ye=C*OYPweEGYfj`q)5Hsy`d-Wx6K>**5TR z8V>^JC{C@$ypKThL0(;Qhj&R(U4L3NM|z}!h($Ik(bK1J8)wb^Db@TQ)sZxiwD(I4 za?^i@L-}E*kf*H;L+9nSR*vWu3F=+@i|uvV&odyrTc^dM0s@d@Tz_Wyu{?tyC?l@X zYB-t;J_>)E&w*eXYV{Z+Nq=<`5%N=yX?Yf({WU)X^zLm|c89123vvxO3`uA$2IS?A zjgWg2>4&RgKmm5DWkFQUDM<{~;eLp1yKEPRDwFcC`nZyd{X`&aPOCCgIMO$Or;~5D zPy$_9LoyK44rLGmSfJh04v2IuZr&RB8=_G#oQX@-wb)9u`}0*1NFSW3|ipr zh&iHv=(^Ez@sp&HWCFAHJQ(PM7=F3xpu)LV%_f?<(~~AOSNa?oxpM}mMnDq#&pQwj55|d5&>O^ z8}1hzKy3%D+*2!jiwvhVVW5cD;$<}H3~#iy70?-;BkGj=jb&?Kp)yrznBK-;B3pc% zDLKAgU$1tyTlDi_mLT?Qd%Yzmhbr!1C4H2q9i}^$MRB~+Y(MXIIJjpTD7frN*1URf z`wHWR7O~O$;SS$cRS0~`SOSAD(>9e_xPh|HQf*~TPa>(Q2Xzt?-9oeXLAmX0+h z-TYglTt1@n!?-7ox`%vj+kzc|YjRqJdXl{`q`duP*K}~)LF@O!pr1!zI*%zgJ(r^2 z87W?1dZqnx|D%UKDUY!>>s|k~i9ep(*}UT1d-oqTnyHI8`*WxJpODN?#Sx3h7|DdN zA1P?B*9!vPj>Uc5bv|cf7kaJ?IlOCYys`OC_rZK>%mS$b= zF-X|Z*cglAs()?5`Mz^NKQPHF>$Sr`)3~JIw5fvw_xJ1;k9%l2$adYnD(q~D4a;t* zf5$l%_v;QYLq*fgXTIYwZ`YfDHHJ5j@@lS`(M}`yp9$46ny`8KxK5l-YgcSR!#y|u zXh;KL6&-WvLv- zVEx!?!?-nvnCjbjcZd^{lLW%0`*F!z8?*F z9q?RNldBHiFoF_aZZw_{u)^QpmBOlxgbHlTl^M zIc@D7WM2MO-xtHiD&)G`O>JAs-iZ|a6})x)gz{^E{3tq2*nyJUs41hb?=9mS?P|@? z?k*pSU1dVO6%+9odk$lD{T~*NM3}~bLVdq+w>!iIqJ|TY3xOKtF8(SJk~nG$T3cro@{-8D^wPyI`wDSo3Gq*FH<3Sc#*im6LrI=h_QUw+W{kkG zH@f6xA?NyyU?6eXBYsGYDzE0L`t3&?Mft?5AlbkNo6nH~I$;^X0uJXF!DPx*no3=cOJB~s8bV{*bmAnzm=&6Y{AGlSl&`sC1huv-t9P{(PR zuF?ei2W$ab^|9>R-E9N64=V^Ws5(1GG=F&RP^PpR|9tI0yt)QK0YkF)fhmyA2AdBT zy4=?8MC)m1*06&|dy6+>Q1dVs!He;YYX+0bsfsW`r?l=EInqEOIna0PDEwS%^E{kk7ogMx~$Je)6%p3xHS|7pUrgS_wyeA&4lDZdVfnG=`IOb9 z<7QHdED(ya^q8$lWSf_A7Jx>`^)m~k508e9nhoUiqTXlW+Bq-WRC_L;Hyl3G z;}&b{%qHz#c&_{ie%O(T$d6wyxf3Zvla!dFXR}HvM+9By7h?WLs;84X2%Ua zFlhS~?7s$_1nREs=i2Vu=Ks1tp^sLx^^?br%%w*-^@;e@0<~Q16}33^kn%?0kKva>2P4h) z3*JY?=|K0x&mnGZ)NlL`&kr26_5FsBJFpFhUHg;6^ucWACwtsZX(!;;^5rY|b~=+M ziCJ?<(RdOU7)oP%6{u7(FVeZ^g?9gqR>puQ_PX~E{!T_|Zb3es$kYWc|xAQk7OeXlRFVdTlzAFGZg@=fsvYePib=| z!R@{YJxCsXpULd`eJ(+qE3w}YNP_!OCfQO=Y}FUg9d>GV%ZW>de{HIY)Xx-9YwlOg z4%e38o}Ue6?p}H=;R(U_WVTH3@1n=u>D7kG74O1Z%}7aPi&ZXw&5!EhBTw(crsPYF zO0Ai?Pb7bj4$95G{ZyUsOI5*u?O$dZZ1rc0B)4FoTK`xnz=*PN-Zm84vR<3R|>zIx5PXP?xJk^|gspQADHDIJZ!a<1=)@wdCmXSMsd85lny3VNBg`bFvm{NJvPWH@I4Eo3--o z7sPIMHkyv-gXM=}te^Tq+iyA&va?4!QLwx=BVRQgPX>q6(z>3=HLE%k=RPhpkGV5C z6#lrc_|oB&))h5?!VDsG%taDAgq{t~tejk&^JGP`ycJprKx>JmsNM@f-6AFCU60dP zbh-bE=4a}xVXnRBE_VA6?Z@w)HRGVWVW+`OC_Oeuv>oA?2Kn&Ow#4Uqd*8ZlRvyCNw_+LK;o(6nKIp&Ia`Fu6I?( zkIeu2oUCpeGy=u5hpo~)+ldk#WjMJv21#a5bHME(@sPyzm&h6k2#eQ2KO_q&9C8^n z0u^#>gTKhRBbW3M3;VAXhOnG6h`_wl5s8<;i=^~dZT9Y*9#SLBjnp1&yOCzTZ8|_o zB=IevYlq}yzG3S8&I#iWV2;lZUB(}PssX@WI4u|J;h#xVsKZ>ZkL4(wh_$+e zmtI?vRsx5%whWt<$NI{&{Q~qbXN?8(2_*JK>z0Sm0KNt1Ta{jQ>+84)4wo^|1RD1u zLw(>IaJ!t2OobRwnlFyo#5!}1-~%Ko>YEm0w|(}s#;)Ggbd5cwooI-?*G^AeS%f*M z$w-oQuB5}N(T!t#-B===YE?t*z@CAS8&HU{suK;G7{n@M&W6?L1?ouYyQjU+HJwMO zwMz$D09){3vQ^l?P@JCoyo7%BtL(v z6sHnBVXJA~WY~C&+Nc=iUi~f&q|CdvxV0}i!PtF^k~pzL5U3vEr^{}u#`%Ti!O=26&*6^*j29j>_Qn+bhx5 z>)vox_v_M1?v9YmKiyk^bOl=lV=s-{n@%m6Iwp&i|D65)XXJld{OjS!u%>l~Aqwj} zmiyNC;-_t^eN!oQ+4C+c-J&A$b-rleY33f_X00@~kiUB7o!1ii{7&B@kydW=d@)di zjeNJZSlaKkmgc{=olhIK0$0pcc9LE8w=aH<*2Pb2LBqLbfBo^Y_VJfdX)w>b9$}R# zG)4nBvA~#huvZ~1adSi7#AbG-c?-ww+0Px99#MsLnK52g(#}C`qR;E=77i4`BNu~# z^iJGs#cbrz?_2_FYJNa}-}+zybo8;W9)}G3fY1G8IL|Muw=|Dn%Hj}e470C{icc>mAw@xcQxC%5J9I0ztN;B(xoA;LE~!^RBGz~QrjWI3r? z^yTLl@8+~EU+@?x#e?NfcLx+%I#SUyJ8#sU?kMA+YgTDlY6msF27`n(0Z_^hk^dWi z!{lzBUcn~N+C@DMR-4SXU9SD{WUAfj4#MrV>~_{o^|c3f&}i0E%(v^^7hWUv9 ze349qF1g_|n0z3CwWev5(ro$AzIk6fLTtZ0aK*!#)H8B!JPjhXuR;0*DfJe=p^b`) zBJ*ECQptcumeK~Ml&Df<@4dh3Z&p5fz!5t)ZS}$+HFoD}{1+xFLYt`POrs||GG^vP|s2uWHqlwqQ^1nU>Nt zAniNeGupxQ_Hd&W4aMbN6iGejk5DPjX+Wn#mUk7Y4!t4Vxnn%vxx ze zZsdsDksj+Wvv^2h`C68;_}sIp1MC)|58#NG#=j1SrW-4sdr*@D zE@3y<5(>0%091|yIz4&*e$fOE2fhEBTZMv?q)co}uk zn(eazl=ZLFps{V^$hSBfl;f2To@3F-goxNuJt@BzYwB zralK)Ph|=IFEyLS7dT<@4`C{0Vv^f)`eaRx;C!Y9`hovfRga*iL2`^%eEv~9V z96Y}@PdbJGV{UFHbJ^bdK=n4ej#kf&vZ#U0NM$$Wj(=nLPtcv z{Q~UN2_9LKZjq`jT zv&8l3W&D8IG=ECwZCr!ay|Tq4lcZr- zN+z-g!hx%w^Wio|E>qh(O7HeJf z^*grKCpV7-=m8}3OfFfo~c>ylUK`ypb)M@m>ij4xl(AU@rdY|7#@tnK~qwQJQR1h zll+EsrcAxM6V~B)RfG0NpFupufD?MT})VALoX?PS8yi;vt^D{;{6M<Z!;!iLp({G_l|QE4!S3P%KfM5}zUY&^rHZa1f1Af^xwduqVMr9EvM`_C7*(I!_e zVKns>y8ZOzU%f79lOLx>&~tIAJi5{vPE=E9F;2aWY6NYG85|N;sZ7Y=)`Ogg&Vz!! z8K17q0BXSXrrhMnp1IR^YZ^2ma%SCP0W^ZX_hh7hc27lr#pO4F?2IHWFxULf6E$qQ zLs+l$2Tt(V{kPn%g8)1yl*W_ZOj9Fj287=$Jm8evgvzfl6okvk27>(6YcmF(t6gJD zo=$!)mHuOU0_k$|s2ZBsLL_9s#xX~vy&%`FFVzS|YhX0VpxX|U?a7gG2n$*Oz!DlH zy{KF%%_ZG|2FL_lN3Kko3zi^^f9Q{OyIx|O=i}_JaSbmUj6nVqA6m3^?QHU zC6a|hA(l1xPydwj_)nJ@kBD4|kegi+4$p`}a)%LafH$7+PNHRBU~#vewmzbWq#X5= z-4x#EJ-JU(283jWHV88i_{g$76 zU6v}UL3Ye_GlXBV=C3J-{^0QvF5by8#E|)#*l58`zHT_dElAmZRpbEd2LyKA3n5%J zz2~Fc6e9?AVXMDANJ-<#56;r<>YkaU zua<^`H-e({wTkhn;JNSV!>5_T0kUt3W7x3Y5zIAOAQiLIeXaRgmrqABt?T9kW%nlF ztin|=*Ox8kWH1(gSIZz3N9fD~^CdW!wH@F0K#KRk}Dh+yOK9RK;aCGSvACgs+SF+)zAm5 zVFuzbp^4ObP{a$*T+&xDN<>UEijYtU%cgohCS)31+zV?z+`H=l0D;gQ$;hBBzx*IwVy&X< z-_)t7Z4oS>Cc6ES3@L3O74U>KCuZq#*C33d)Wc%c>0~Wc?8m=L%A(L^WV?hz%&r0r z8p%8kyt~uHBMq~|@+QpIB({T``iBGd>}tKftKF<;+C~iAcN5+vmlTfx)5avjs|F>w`kZsNTKR*(=hUpO=>}dFJA0lDH`I zA$1L8Ft)7dwrBsj)aoizTIujs`}(^^cYdoc=)9y^RO@+xoj#&1z^64O0vHt%Nz}Ye z#D@b@2BgX)m!S&}WOXjUkU94QjYTk7H3tj&>{N^~|J7AdqwU_T&1_8<2_qt) zX&8Yk(pyCBOPcSi6Min54Ge90y*%ibTUfrozM3NRd{11(`m$fGso(hZ<|JI4@k8f_ zDqQa{j!SPu9@IGYGpC_}*z;v-FWu=7v%B_)4jjcGxBA!&k zO*cQZ-Zsjw&rLqx<%0S2d4`Pc1owHzAR9RJWcFe$K1IbZz9GN;F8C~V-Z}UsxHuIx zGqCXBC5=sqSXh*M-8BXq69JfWx}ga&$eg%jIDe2F1d-}Q7%+PaBTQ9dK`kCZe43-V z`VszT5Z+@7@2O7F=VCV3jl9+R*X90RdlXszUwZ}P~udUy`S_bYC#7dQGi|}so zfiXMxu6SK0A#cLv(&9|zOi=#>ZeK7x*K<_`CI}h6@*S-e`9~X)={_F_w&gGP$2ELK zwXILdjgCi;_>c#r)vp#WPeONlcWq`UcTYo5#&P$rXJEXfy1?F5vCoXCJ%FJ zQ{nKF4r)rRRx;wIG+IZqkT&tMYj}34f|Dl}sP^iinmw`Q^jSTF+i_sv-{raUAHNfMj-nhFR_520C7QLy%GgOX z^Z_UPv&}Q*oXxLj6$cF)&lsZk!Dg?V&Q{N~;Ea*pLP7OLwb5;hB*K+&x zsYv%j0St|TMJB@_2mVR@<;DAm<2?8*XlTOt^c0kr%&gnvdqPx3P}TWSy?BiRCW-Y$ zi*!NJI0!|lG?(lx7z;zt`~mOt)<46h5~T*uJfLZfJvO4D`Impqe>dlBZOpV%Iz=Y^ z4hd0#fjb!kPD*B*X7VjbSK`SO6@eGjA%FgF$GnR~-$tEsXQ_7r2M4C}`=!>zE#442 zm>%k%#;1N*ZeO}T&W?~7`KHh04Py@0HDJYRoFU}qZ{;Z07b-MrJRTSr)cO__-QVrZ z=&;i!!(E>77tVs-^=gR%#r|mk+yXc0 zgfbp{W5^l6F;}5ht+!|w1lIMv#S9>sWcImm_V(`>SS(ZgIOY&ty$e26j0vOd{ID-k zyWFO?4xaLNF~QEbu`!p1HiICNLQV!rNbBM=dp}RuiCio};}GZwT<8QEXXMPnWiYy` zZBy?*?2NmdtHADnH!S@2DCvxoNT(oo`_yV9N!cuze{`YoC@iUn0gVN$|UNY~Na>x#o!bcCZnw_G0nzYKVX`OaSTdm-O0IkK1R}c4nkZtga zW$oIZ)R!o{oslit>8v!vr@cGF59{iphowtr*5}XMfzTW5u1~*z z$hQvvIWUsDJEUBh>`vj(eJGV!!T5so=2Y$; zo6HM#T`@W{IHf+pF#sFA?S6uZrI|44RV&SQq1erx1zQ89G?lkmIoOedXUTP$ftx6Q`DBq&jjjpqUd7R=5e7uvzWP{rV< zUHSHjTy<6%Jap3Uz0tBz2h{zZ7xM}BRSySUO`QRVu0uw*y_3vp{aGwYSx5)*Mx|)@ zV?!`#p<1EV_t51};C1V4Vt(Fxgs{$7Y!d84`@4zFSl{ESFxd;&ZSr5K&=r6{+^udZDSV1HJ;&wmMenLWfdK+g2mF`RIx|2Q2`zxqV_UmI$jsmA7h;~aXTq@%sbIY~Zn z_kGbTxjTw!-yElm&VJ7p2RhaldGN~%rblMsp}Bi7FJm>SORV5_varge4%`|MSug*h$)qLLZ=wTqaYr(jLmZjHjP)5J*L zcvA!nnmPx$WVEWF%*mz$OisH-{teosV^7mELBehPyNheMCS&QTH_Rj{>jLAB-lMTe zSH2L`@xv~Px6qD!eNwF9@#tvS@yzq5^r2nm9nOaT=D_Zo<8{I%T(0oT&pVv_UFo51 z2e5B6T!mq6)hI(t<H|r6Rggbk{pdG;}?;HYwSmuA3X7GDdj|qGaCoUB#7?@fHFHzuaUIN1&&8T zEaEe$bU7&%tnB9+S%ov7f(f#zy`!K=3yIKZ-9qq8#-zd~B7teP0U+Ig6{Ti&{$HOt zZ=KV#Pg4!972&rO!yJ)*;BCMT8oe9?3;KL3#?V_F%_K=&-LG#Qj~aI0%oR?Vsb;!# zo}}eK6;L%JWwQti1I1UC@K|%;O?KeluIYsd!Rk$TkDRR8x9;xv=@@J1r~hU$kM0Bl zE(=eMJ9-xG5PLZb0wewD!ceO+OlX&W;YhJj#xd7eR?vCaAWPo=)Nn*;LUJ6x`D09L zs>5;5(}kD(oRpB2F6isWA|NdOY%*kO>Lx?V3Q)2baE||CO{xdZ_1SjGJty>~b>;k4 zU8tKhHh2re|IygH(IYo2jbbZkk@C~$PM1=+!>e|YWOvS)%IBaiqGKVE@iyi ztd=nhyIeSIG5AhMXA$E+(YU%|LZGdl<%J5laGgDI(%Gju9r@eH>@cql71z&_U@qO)X=*7@8Ej
    !%6&iTj#r{8+IqpaV}2E z&Fmg6n~|R#jcK#K$=oRC=Ff(dXmu$OxTMG-!dvdNlBA-pCITJyWV(CHDh*H8zwnkL zR}2c5!Q}b6((R8zSXr^%(DxjQW}dod?G`ThEk>6l^0xaZSEikgjd<*57Z&cs z+Jh~R`yaN%Yt9bOb_{}^_Gh8P4Neh%P+Kq_NlqT+m9O!?@b48kdr|?H7yiXzH`;p) zV^}V=ThUXp1Wvj%*lddpYZb?g=-N#e;G}`UCx2`>akMJt%A+c@@%?XQGfKR6FZaFb zY*?*MBgTW$KVjt80kE;@;15}<7OzvdaS)(LxAIdV*8J=y1V`(RH^Puq0phMeA4o_< zs}UC}-}2`KT176HIROqsPyrO>6A0oK%JVn*&xVY{Ph_Hi+l{fa#+C!^u- z)T?gPKqb{c^4A=?zua@$icFTXQR|-`%RTs)faB)8S5f@m4v)}D zKH*ipM2ydKie8_HV0$EPvh%QX8NE(DF;boXK#{z1%ki43W}034=kVOd!wL;E5SOU( zA~(1R|LHgRHCaBzWkqDs-<7(=BPdLzJ*kU{Wdjz_kI>*+HlMRv5ihSI=5PWzNK9-D zedBxWk6A*v29z{uaTzE9$&GHElXdcS&*u%-cS7Ndb+G{Mc9zr6ZPB#ocUueJX-A0K z*u2J>Z?>XF(kT~YCe6!q(B|v-*W6hv_Ph$zd8-hS4~?*Mk?IE6IY(}ky{FipR-*as z70puCaJ`bnu_O(tzTAi#&&VKr-}N;7XAi1>$uh*7!4l*-j>AcU8Sw1}qX+x|33O~i zZ#?U5=)q2%{ZU>B@PKW<#?*YjdAh2&F`T#B+t&S+et@?{*FDvK>|h8Cnq8RQ?$Z{kpOElg9p9(qCHqYh>@odGgb)&x- zUiGe4kr(b!b-f0fJMACbBn*P9FO6haH&21(!3F}`x+1~Q;u=>u)I7WGpkO+y+~4E( zx3Sp#;1Jx4!iQd%ROkeE_StG!bBNEleqIDJ?G%GVddve_KvV)2Ax~4_gYXCNfSm9jD7tJgqfwfI>GUd(N#N zqw1Q7=LAS>{3XZzD}dyjX-Al>3a%57Ay^~YQ#x%|O!EnMdPB+2bJowZ1KGl8E@@f( z%AI>J+VG=s*GnHBg=i{+hFFA<44$~1VEYkN@=BW>Sg@9ELD2soC5lI1VfdKSYrovm z1&k?CZ-rx0coat7GzdJH<55RHY_J5cX?l>)YTW1i+y8UoFl^8cCSH$ByyxavPiSfx4W13)#Clq*n!oI-++`zIb#7b9GBUNX5GQSgrKqD zl6_tM6U0<*uC3vbIINGH=paVoTCpyWaQ~9Dnh5@{R zh>q;d#-Lg1UWX5ulqxLEHZ@K4a);wEw2oTN^hsoZ8JiyA^atfSTsS!^8VEDsc;7pH zN2wpLS{IhFw`Vq)i{4Ob&VfeyR>63Nib6<9*!EQ;FRJ_v$*Kwd=~`&(7b!=>xTi7q zr7kg{N|NI_`SmrP$206L&RMI;8;yF)g;zORKx9~fVE#FB59%|rZoLSzE0bk$f^eT4 z>Ssr;p*Y4XST*Oaeb(*{UkbAGzjg&|`p z^<63(g|t9)Ma2dEU>fUt`oC)~6AQpPT8}C9N3XbsF9B^jW#LyL7oco5Dbb+AZR9IC8o%9ac+1h9RVR^W^SQt`ZA)9V=+p zJYJ{{QNPx`QL=s9f`b0L|I2GlD7s}JpR77$w!ojle)nJjEA|VKd`<#^%^#mDWNZB3 zH`-u&)Ao9iXR%+Ln_+}1-H;PUx#}i7?X0281L+R{g_r|j2nl?m!DRPY$ z)(K@o4&=v<;i8pQxF_lV12T;18|t6cow6{p)%k|l-@)$cZB`v8apVt24wTPiZpc$#AC2Znwv~WI;>XPEg$&tsmGdG zy}&^??d?=AzIoZ4`>6YW5}Jj2Wer%S?B!(zF5xw1AF+{z3<|f$mb&yThZ%&% zfGLB!80|ko4)bfiId3|feMdZiXgot@79=71uqCT}?9&opTU}XuS8hPyf8-yq=k}Xj zHREHqxalnbK*IRne*}m=X|vDaHd_XY5zQaCEsx$Syx-Fb33$CznWx$8g?YxAEQ5e0!(dr#zS$36S)|n{;P0)%t*EUapFK910^#4pEZ2m(;07AXB+gcj7sD6h4*- z`8!Bfq58)x8DiSKvpiC*Q!61g)bQaw3x7Dq$V&d5&ZrHDUmPX(1Di>cHBu6a2C^{7 zzQ^2*m30n$F|Uy`?ebyBZ*;}N0QG%+iE_RLE33HP`-LC!1|PVhj;{d~mOkUOr51@# z0BWyFlVth386NpXebd z6@E823ZmEE+*J3-hy_zBxa+%#m4g&Sd_P+auFZP(gga%7q)U<=8~u{)iqr>0tTvmq zt>5$Lafq{ylJl3HIES7%%kI$(LhM-feRj+n3OoPEUG&9iHG3(n&y37yyVj*ghF%|r z+?3fDEq5*KxVu@s=>4D+`Ir8^&T=oxrNR4DSzI253P~mCJjZ)M1{A9=Fxu7pXAMTqUt`p*13>%z2)2ghgRpC>jgO{ z{QTl@7Zwml)$#WsZk6|HD(M zXKrvS^+~8mk)R~wF93kdq*6~7t`>077ySvKuw$@1m`x}SdRP7VUA|HxrVhCrIF&bxO@1I_LTvz` zW08;hWyUGM<;vfJq(Brn_mm={Ec^;i`xz!k!+P?QQrIn9e(qgOq`fh&As7=P?H_bV zXLa7sB{uoiqxC zou>!bTu*obI{jmg8oXSi6JOx5#YM%<3z^BH?ft9G0*yXCM$d_TlV~e|RZlcehF{nK zkk4_+*;zODj(5j6#w3oHbn%E5ZgQTx&D5We`j2HFczm+ubv~$vB9z=>eaKPshmjjU z&2)bBQEE^O2rq7xy21J#5`n_7fM7a;W{D6xSZSZMQIx?jYpemOM;sVtd-&Em-{GQL z2UJNVN1&S!C5loZT~YyuW{$us_^w*eL*KqPTN8Yk??zILaP3X4Uaq+jnooT)Zd3SF z?2UWI^JNg#*itQL5$R#5@CndS!3x0vs6#1mROP*TS0p}(rkvevd+t4L)edd3lj}9f z7Mq^%q0kG(V2!88*gM0qx*iQ^D7$HD`6=DR&->&`?vG^eNiZri)bB(uQzt><3XX^h zgV*QSS**J?540$#JRfge+SaKk;2DKv74vtAHKcN+f-HQ^N~thX<;7a+m+zA!w%bhq z%dl7$caCJ*T)w_+ngq2etWklIJ59TXx?h|o-|5DNgx=w|Ah5$dSNaD89BjeCDf5-3 zp6uTDKVI|Yg5yIbq}MVySM(h!#?nbAWrJ(`~{HB{hk5=~Pr zQ(^_5&INAL6!;>HE8#VtnFc^W?kRlh0}bF|{4?QY2cjpbnXR(WrgPfw^^Qk9%NqDh zJ{RY+!6gVgQq9Xj`N{7N$SZ#A2} zJw1bD@9BRYuYcz`u0}W^tmnWHsQ}E^p5%|%0Tcm}x%CJlV=_-~cE|jDy0;DABii6a z$|So3Hf7qJdHNoYmBo1D6_4{KS0n+F+0c+}QaO(^sBK{RN6#J6kh?3CLXBLp`Am?d z;c~A)T;3UQ&V;O2p86AcQwUZHjlu`9x3bcG;STh&f~fM znL9aD)v8cCRI@kv(9Xn~w^B60WOHf1j6RVZ4s0;vJh@#Kraw%Ilzcv?b-%h3K zErRjp&+-T`5o9te5HFaCZfq^ zvmxP>#vCKIHs`i^P)BGl>R0?uHdnhSXCSpUWOR8uHh{fotLXp3FBm_COYzoCd3dUOrHQLxWVYMWRNXQThx8$S=E*IZvgs#tWVvN`&@XaEeZij4o&4?llencQ|m zc>u(Dp}qR~@A7aSwWa?(=vQ{(e>%IrlLP?~Z7qBCA*{5M?iXHe~{d4bb1 znDi?zEvaF*a7@u+%}6AQ$+5>U!Y~!}-t2=-NIdu~^KF*<2z!wchmBo!0s_LY0GCn` zMDZcE9dajTzcDOOivk2mGk$zbdk8<_l$KJ}LiQA?-b8cIE4LtID|vW~x^aU{Bann3(04qOpE; zI04%;q@9D~byNzP@ufy0(-A##YWCXYXXi*rAqv50msju8wZIP$}C=8`fIh>gAj?&cOr$y8V4j=f^iXGDF0sg zEkRyN5wmsCs(>E@u=`mfhn^J>)N^rAUiZ;(sEv4`DbC3YHBN&=W0Bp1&9ty2(UtxH zN#srY$d$+8^-WFlI()fn7{cb zOCdMiC{=_}NxRUmtttIGn+uqRWzDZ}k{k}SkaD&E3?$1L(AgxRs=)?0B!Nz= zvus`U4?vD+6siakzpB1)`R0_a126GGO0ktG{(Iioba!;1$|q&?fO-=wQDUMndw1gf z2QX-LPm*SHbjs$$hbRWoa{J^GLklH}*^@6rg`J|$tKP=+`pC^hNbQiPUqZxy!JcLr zj2<`qqL-gcfDyf%3Q|4) zLc0!bCCZ%QT>%}f2xMDyAx1!tXAqI#Y2kO{Ld#Ya+uJm9C_J{kPE)x>b<>~0HVtT+ z(u54CIzDC1rwq=;a8(;WxB4hc7PnmSwI; zZ0J&hzMyKOMJ}BFOQzP~`&yicN7qQX;ECQ&xD>D3Eg;_<*s&U&KJqe%Fxk199`(Rw573V; zAe2V2SI^zWH>6tqI@p5r3lxN|32#Q2?&=ebc>^*{2rU|$Ot5s?kDRI$5zOz(>nM|C}>_ffCjvLG17p=kbZ zIwmeg^nJN~@r##L+>Aje)Kzw zpBU^yZy3Xfy)z=i{}2xUvLc8lL0kvW3oVD8dlI zGNVB^9=Q~IessfT<8qzBOw!A=RjLTY{*YJ_-UqV#Q9n8pJtSuqQR2uTho}xfyV_Yt z_d=lTb3UCB$E_@BGOPjL*coP9X_yTLt&kSv2B@ju5%uZwXFeFmCG`9HlO2DJbBYpe z^T4Qq>^!?;7a-giJj7q%jK-}Bv9?@*u73aMroA5V&H&FCW!8MHS;-p^6X*AgbO3ZRtnD$=ihT zB}<}MYS`6ewd^`3RyBVo%@?MzyF+Krk|W{ zbm-~Rd}lJm0hyke7^F}>+kYAsM^&Z-4M>QaX+^UU_h9$*PFdNBk(IF>lqHWfpNum8 zP;fx{X8zFkPryxuzxtJmV?E(9IYE-mG*^ zYo9*G+tG&;WcnEos`L1Gx)sGPAj@FVhez9g1Rx3mt#{Y}M8Sal9*GWN$WWmFuOhi< zHYPL>fgC1bL=gZ{(Es}O27o9O-u(EVo8|uhoB!Vp7@Hvup!N)BoklfD*@g!(q*Q!T zG#$>$4_{V;x%c66s%1o&1!JIFO;-AFQIuMY zYj^V#rEzxC-VNijTL*d(p8Sc-5Brw)?WFrtU&F{Xb;zdg75)a{LI{qTCQm(TLJLA^g>p)>%N7dDgo(N z({|iGGGJ5fm*-Oy0pZwQU$%s{i)u6FO)0s*Wvr_{W(P5%S?nt~pw1r30(Cjetx~m< z+o9EFoNM>np*LISx@I9|$(Y56M-ZT!Ad3Ui!o>;c!(GOmvVyfQO`VY5)_g!|bBJT@ zTle2g)np~KpXtEwi9w_h3%xE-SHRp#<^{b74LJZD|B-2g*BVtZZ)(9WsLyHvDvkhg zh8v{t=r?1+!!N|5zr;!~frmi!ta(+G+D|KOrfehH6FXzB(D U9TXt`cNXwoQC*=%&g|>|0cm!!kN^Mx literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/opnotes-logo.svg b/src/renderer/src/assets/opnotes-logo.svg new file mode 100644 index 0000000..4e65b96 --- /dev/null +++ b/src/renderer/src/assets/opnotes-logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/components/brand/OpNotesLogo.tsx b/src/renderer/src/components/brand/OpNotesLogo.tsx new file mode 100644 index 0000000..f4dd2d2 --- /dev/null +++ b/src/renderer/src/components/brand/OpNotesLogo.tsx @@ -0,0 +1,178 @@ +import { cn } from '@renderer/lib/utils' + +interface OpNotesLogoProps { + className?: string + size?: number +} + +export function OpNotesLogo({ className, size = 24 }: OpNotesLogoProps) { + return ( + + {/* Clipboard base */} + + + {/* Clipboard inner */} + + + {/* Clipboard clip */} + + + + {/* Surgical scalpel / pen - angled writing instrument */} + + + {/* Scalpel blade highlight */} + + + {/* Note lines */} + + + + ) +} + +export function OpNotesLogoFilled({ className, size = 24 }: OpNotesLogoProps) { + return ( + + {/* Background circle */} + + + {/* Clipboard */} + + + {/* Clipboard clip */} + + + + {/* Inner document area */} + + + {/* Medical cross */} + + + + ) +} + +export function OpNotesLogoMinimal({ className, size = 24 }: OpNotesLogoProps) { + return ( + + {/* Heartbeat line forming an 'O' shape with pulse */} + + + {/* Subtle pen/writing indicator */} + + + ) +} diff --git a/src/renderer/src/components/command-palette/CommandPalette.tsx b/src/renderer/src/components/command-palette/CommandPalette.tsx new file mode 100644 index 0000000..4a30150 --- /dev/null +++ b/src/renderer/src/components/command-palette/CommandPalette.tsx @@ -0,0 +1,211 @@ +import { useCallback, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from '@renderer/components/ui/command' +import { + LayoutDashboard, + Users, + Stethoscope, + UserCog, + Settings, + Plus, + Search, + FileText, + User, + Clock +} from 'lucide-react' +import { queries } from '@renderer/lib/queries' +import { useDebounce } from '@renderer/hooks/useDebounce' + +interface CommandPaletteProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { + const navigate = useNavigate() + const [search, setSearch] = useState('') + const debouncedSearch = useDebounce(search, 200) + + // Search patients when there's input + const { data: patients, isLoading: isLoadingPatients } = useQuery({ + ...queries.patients.list({ search: debouncedSearch, pageSize: 5, page: 0 }), + enabled: open && debouncedSearch.length > 1 + }) + + // Search surgeries when there's input + const { data: surgeries, isLoading: isLoadingSurgeries } = useQuery({ + ...queries.surgeries.list({ search: debouncedSearch, pageSize: 5, page: 0 }), + enabled: open && debouncedSearch.length > 1 + }) + + // Search doctors when there's input + const { data: doctors, isLoading: isLoadingDoctors } = useQuery({ + ...queries.doctors.list({ search: debouncedSearch, pageSize: 5, page: 0 }), + enabled: open && debouncedSearch.length > 1 + }) + + // Reset search when dialog closes + useEffect(() => { + if (!open) { + setSearch('') + } + }, [open]) + + const runCommand = useCallback( + (command: () => void) => { + onOpenChange(false) + command() + }, + [onOpenChange] + ) + + const hasSearchResults = + debouncedSearch.length > 1 && + ((patients?.data?.length ?? 0) > 0 || + (surgeries?.data?.length ?? 0) > 0 || + (doctors?.data?.length ?? 0) > 0) + + const isSearching = isLoadingPatients || isLoadingSurgeries || isLoadingDoctors + + return ( + + + + + {isSearching ? ( +
    +
    + Searching... +
    + ) : ( + 'No results found.' + )} + + + {/* Search Results */} + {hasSearchResults && ( + <> + {/* Patients Results */} + {(patients?.data?.length ?? 0) > 0 && ( + + {patients?.data?.map((patient) => ( + runCommand(() => navigate(`/patients/${patient.id}`))} + > + +
    + {patient.name} + PHN: {patient.phn} +
    +
    + ))} +
    + )} + + {/* Surgeries Results */} + {(surgeries?.data?.length ?? 0) > 0 && ( + + {surgeries?.data?.map((surgery) => ( + runCommand(() => navigate(`/surgeries/${surgery.id}`))} + > + +
    + {surgery.title} + BHT: {surgery.bht} +
    +
    + ))} +
    + )} + + {/* Doctors Results */} + {(doctors?.data?.length ?? 0) > 0 && ( + + {doctors?.data?.map((doctor) => ( + runCommand(() => navigate(`/doctors/${doctor.id}`))} + > + +
    + {doctor.name} + + SLMC: {doctor.slmc_reg_no} + +
    +
    + ))} +
    + )} + + + + )} + + {/* Quick Actions - shown when no search or always at bottom */} + {!hasSearchResults && ( + <> + + runCommand(() => navigate('/patients/add'))}> + + Add New Patient + + runCommand(() => navigate('/surgeries/add'))}> + + Add New Surgery + + runCommand(() => navigate('/doctors/add'))}> + + Add New Doctor + + + + + + + runCommand(() => navigate('/'))}> + + Dashboard + + runCommand(() => navigate('/patients'))}> + + Patients + + runCommand(() => navigate('/surgeries'))}> + + Surgeries + + runCommand(() => navigate('/doctors'))}> + + Doctors + + runCommand(() => navigate('/settings'))}> + + Settings + + + + )} + + + ) +} diff --git a/src/renderer/src/components/common/TemplateSelector.tsx b/src/renderer/src/components/common/TemplateSelector.tsx index ed94bb4..44e8f6a 100644 --- a/src/renderer/src/components/common/TemplateSelector.tsx +++ b/src/renderer/src/components/common/TemplateSelector.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import { useQuery } from '@tanstack/react-query' import { Globe, User, Search } from 'lucide-react' -import { Dialog, DialogContent } from '@renderer/components/ui/dialog' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { Dialog } from '@renderer/components/ui/dialog' import { Badge } from '@renderer/components/ui/badge' +import { Button } from '@renderer/components/ui/button' import { Input } from '@renderer/components/ui/input' import { FilterCombobox, FilterComboboxItem } from './FilterCombobox' import { queries } from '@renderer/lib/queries' @@ -160,7 +162,9 @@ export const TemplateSelector = ({ open, onOpenChange, onSelect }: TemplateSelec return ( - + + + {/* Search input */}
    @@ -281,8 +285,17 @@ export const TemplateSelector = ({ open, onOpenChange, onSelect }: TemplateSelec {/* Preview panel */}
    -
    +

    Template Preview

    +
    {selectedTemplate ? ( @@ -323,11 +336,18 @@ export const TemplateSelector = ({ open, onOpenChange, onSelect }: TemplateSelec
    {/* Footer */} -
    - Type to search • Use arrow keys to navigate • Press Enter to insert • - Double-click to insert +
    +
    + ↑↓ + navigate + + insert + esc + close +
    - + +
    ) } diff --git a/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx b/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx new file mode 100644 index 0000000..1339fd2 --- /dev/null +++ b/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx @@ -0,0 +1,70 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@renderer/components/ui/dialog' +import { keyboardShortcuts } from '@renderer/hooks/useKeyboardShortcuts' + +interface KeyboardShortcutsHelpProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +function Kbd({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +export function KeyboardShortcutsHelp({ open, onOpenChange }: KeyboardShortcutsHelpProps) { + return ( + + + + Keyboard Shortcuts + Quick keyboard shortcuts to navigate the app faster + + +
    + {keyboardShortcuts.map((category) => ( +
    +

    {category.category}

    +
    + {category.shortcuts.map((shortcut, index) => ( +
    + {shortcut.description} +
    + {shortcut.keys.map((key, keyIndex) => ( + + {keyIndex > 0 && key !== '/' && ( + + + )} + {key === '/' ? ( + or + ) : ( + {key} + )} + + ))} +
    +
    + ))} +
    +
    + ))} +
    + +
    + Press + / to toggle this help +
    +
    +
    + ) +} diff --git a/src/renderer/src/components/layout/Header.tsx b/src/renderer/src/components/layout/Header.tsx new file mode 100644 index 0000000..cde4113 --- /dev/null +++ b/src/renderer/src/components/layout/Header.tsx @@ -0,0 +1,68 @@ +import { Moon, Sun, Search } from 'lucide-react' +import { Button } from '@renderer/components/ui/button' +import { useTheme } from '@renderer/contexts/ThemeContext' +import { useSettings } from '@renderer/contexts/SettingsContext' +import { UpdateIndicator } from '@renderer/components/update/UpdateIndicator' + +interface HeaderProps { + onSearchClick?: () => void +} + +export function Header({ onSearchClick }: HeaderProps) { + const { theme, toggleMode } = useTheme() + const { appVersion, settings } = useSettings() + const isDark = theme.mode === 'dark' + + const hospitalName = settings?.['hospital'] || '' + const unitName = settings?.['unit'] || '' + + return ( +
    + {/* Left: Hospital & Department in two rows */} +
    + {(hospitalName || unitName) ? ( + <> + {hospitalName} + {unitName && ( + {unitName} + )} + + ) : ( + Op Notes + )} +
    + + {/* Center: Search trigger */} + + + {/* Right: Actions */} +
    + + + + + {appVersion && ( + v{appVersion} + )} +
    +
    + ) +} diff --git a/src/renderer/src/components/layout/MainLayout.tsx b/src/renderer/src/components/layout/MainLayout.tsx new file mode 100644 index 0000000..4e85dad --- /dev/null +++ b/src/renderer/src/components/layout/MainLayout.tsx @@ -0,0 +1,52 @@ +import { useState, useCallback } from 'react' +import { Outlet } from 'react-router-dom' +import { Sidebar } from './Sidebar' +import { Header } from './Header' +import { MadeWithLove } from '@renderer/components/brand/MadeWithLove' +import { CommandPalette } from '@renderer/components/command-palette/CommandPalette' +import { KeyboardShortcutsHelp } from '@renderer/components/keyboard/KeyboardShortcutsHelp' +import { useKeyboardShortcuts } from '@renderer/hooks/useKeyboardShortcuts' + +export function MainLayout() { + const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) + const [shortcutsHelpOpen, setShortcutsHelpOpen] = useState(false) + + const handleSearchClick = useCallback(() => { + setCommandPaletteOpen(true) + }, []) + + // Global keyboard shortcuts + useKeyboardShortcuts({ + onOpenCommandPalette: () => setCommandPaletteOpen((prev) => !prev), + onShowHelp: () => setShortcutsHelpOpen((prev) => !prev) + }) + + return ( +
    + {/* Sidebar */} + + + {/* Main content area */} +
    + {/* Header */} +
    + + {/* Content */} +
    + +
    + + {/* Footer */} +
    + +
    +
    + + {/* Command Palette */} + + + {/* Keyboard Shortcuts Help */} + +
    + ) +} diff --git a/src/renderer/src/components/layout/Sidebar.tsx b/src/renderer/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..5038e9b --- /dev/null +++ b/src/renderer/src/components/layout/Sidebar.tsx @@ -0,0 +1,78 @@ +import { NavLink } from 'react-router-dom' +import { cn } from '@renderer/lib/utils' +import { + LayoutDashboard, + Users, + Stethoscope, + UserCog, + Settings +} from 'lucide-react' +import opNotesLogo from '@renderer/assets/opnotes-logo.png' + +interface NavItemProps { + to: string + icon: React.ReactNode + label: string + end?: boolean +} + +function NavItem({ to, icon, label, end }: NavItemProps) { + return ( + + cn( + 'flex flex-col items-center justify-center gap-1.5 w-full py-3 rounded-xl transition-all duration-150', + isActive + ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm' + : 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground' + ) + } + > + {icon} + {label} + + ) +} + +export function Sidebar() { + const mainNavItems = [ + { to: '/', icon: , label: 'Home', end: true }, + { to: '/patients', icon: , label: 'Patients' }, + { to: '/surgeries', icon: , label: 'Surgeries' }, + { to: '/doctors', icon: , label: 'Doctors' } + ] + + return ( + + ) +} diff --git a/src/renderer/src/components/loading/CardSkeleton.tsx b/src/renderer/src/components/loading/CardSkeleton.tsx new file mode 100644 index 0000000..7f5dd86 --- /dev/null +++ b/src/renderer/src/components/loading/CardSkeleton.tsx @@ -0,0 +1,50 @@ +import { Skeleton } from '@renderer/components/ui/skeleton' +import { Card, CardContent, CardHeader } from '@renderer/components/ui/card' + +interface CardSkeletonProps { + hasHeader?: boolean + hasDescription?: boolean + lines?: number +} + +export function CardSkeleton({ hasHeader = true, hasDescription = false, lines = 3 }: CardSkeletonProps) { + return ( + + {hasHeader && ( + + + {hasDescription && } + + )} + +
    + {Array.from({ length: lines }).map((_, i) => ( + + ))} +
    +
    +
    + ) +} + +export function StatsCardSkeleton() { + return ( + + + + + + + + + + + ) +} diff --git a/src/renderer/src/components/loading/DashboardSkeleton.tsx b/src/renderer/src/components/loading/DashboardSkeleton.tsx new file mode 100644 index 0000000..d07e367 --- /dev/null +++ b/src/renderer/src/components/loading/DashboardSkeleton.tsx @@ -0,0 +1,57 @@ +import { Skeleton } from '@renderer/components/ui/skeleton' +import { Card, CardContent, CardHeader } from '@renderer/components/ui/card' +import { StatsCardSkeleton } from './CardSkeleton' + +export function DashboardSkeleton() { + return ( +
    + {/* Stats Cards */} +
    + + + + +
    + + {/* Main content grid */} +
    + {/* Quick Actions skeleton */} + + + + + + +
    + {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
    +
    +
    + + {/* Recent Activity skeleton */} + + + + + + +
    + {Array.from({ length: 5 }).map((_, i) => ( +
    + +
    + + +
    + +
    + ))} +
    +
    +
    +
    +
    + ) +} diff --git a/src/renderer/src/components/loading/TableSkeleton.tsx b/src/renderer/src/components/loading/TableSkeleton.tsx new file mode 100644 index 0000000..b9905a1 --- /dev/null +++ b/src/renderer/src/components/loading/TableSkeleton.tsx @@ -0,0 +1,46 @@ +import { Skeleton } from '@renderer/components/ui/skeleton' + +interface TableSkeletonProps { + columns?: number + rows?: number +} + +export function TableSkeleton({ columns = 5, rows = 5 }: TableSkeletonProps) { + return ( +
    + {/* Header */} +
    +
    + {Array.from({ length: columns }).map((_, i) => ( + + ))} +
    +
    + + {/* Body */} +
    + {Array.from({ length: rows }).map((_, rowIndex) => ( +
    + {Array.from({ length: columns }).map((_, colIndex) => ( + + ))} +
    + ))} +
    + + {/* Pagination skeleton */} +
    + +
    + + + +
    +
    +
    + ) +} diff --git a/src/renderer/src/components/loading/index.ts b/src/renderer/src/components/loading/index.ts new file mode 100644 index 0000000..1b9517d --- /dev/null +++ b/src/renderer/src/components/loading/index.ts @@ -0,0 +1,3 @@ +export { TableSkeleton } from './TableSkeleton' +export { CardSkeleton, StatsCardSkeleton } from './CardSkeleton' +export { DashboardSkeleton } from './DashboardSkeleton' diff --git a/src/renderer/src/components/theme/ThemeSwitcher.tsx b/src/renderer/src/components/theme/ThemeSwitcher.tsx new file mode 100644 index 0000000..ff5c77d --- /dev/null +++ b/src/renderer/src/components/theme/ThemeSwitcher.tsx @@ -0,0 +1,171 @@ +import { useTheme, themes, type ThemeId, type ThemeBase } from '@renderer/contexts/ThemeContext' +import { cn } from '@renderer/lib/utils' +import { Check, Moon, Sun } from 'lucide-react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@renderer/components/ui/card' + +interface ThemePreviewProps { + themeId: ThemeId + isSelected: boolean + onClick: () => void +} + +function ThemePreview({ themeId, isSelected, onClick }: ThemePreviewProps) { + const theme = themes.find((t) => t.id === themeId) + if (!theme) return null + + const isDark = theme.mode === 'dark' + + return ( + + ) +} + +interface ThemeGroupProps { + base: ThemeBase + label: string + description: string +} + +function ThemeGroup({ base, label, description }: ThemeGroupProps) { + const { themeId, setTheme } = useTheme() + const groupThemes = themes.filter((t) => t.base === base) + + return ( +
    +
    +

    {label}

    +

    {description}

    +
    +
    + {groupThemes.map((theme) => ( + setTheme(theme.id)} + /> + ))} +
    +
    + ) +} + +export function ThemeSwitcher() { + return ( + + + Theme + + Choose your preferred theme. Each theme has light and dark variants. + + + + + + + + + + ) +} + +export function ThemeQuickSwitcher() { + const { theme, toggleMode } = useTheme() + const isDark = theme.mode === 'dark' + + return ( + + ) +} diff --git a/src/renderer/src/components/ui/alert-dialog.tsx b/src/renderer/src/components/ui/alert-dialog.tsx index 7c6ca09..1a4d8af 100644 --- a/src/renderer/src/components/ui/alert-dialog.tsx +++ b/src/renderer/src/components/ui/alert-dialog.tsx @@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef< , - IconRight: ({ ...props }) => + Chevron: ({ orientation, ...props }) => + orientation === 'left' ? ( + + ) : ( + + ) }} {...props} /> diff --git a/src/renderer/src/components/ui/command.tsx b/src/renderer/src/components/ui/command.tsx index b381b7b..fc9abc8 100644 --- a/src/renderer/src/components/ui/command.tsx +++ b/src/renderer/src/components/ui/command.tsx @@ -1,10 +1,11 @@ import * as React from 'react' import { type DialogProps } from '@radix-ui/react-dialog' +import * as DialogPrimitive from '@radix-ui/react-dialog' import { Command as CommandPrimitive } from 'cmdk' import { Search } from 'lucide-react' import { cn } from '@/lib/utils' -import { Dialog, DialogContent } from '@/components/ui/dialog' +import { Dialog } from '@/components/ui/dialog' const Command = React.forwardRef< React.ElementRef, @@ -26,11 +27,14 @@ interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( - - - {children} - - + + + + + {children} + + + ) } diff --git a/src/renderer/src/components/ui/data-table/data-table.tsx b/src/renderer/src/components/ui/data-table/data-table.tsx index d431507..8fa52cc 100644 --- a/src/renderer/src/components/ui/data-table/data-table.tsx +++ b/src/renderer/src/components/ui/data-table/data-table.tsx @@ -2,6 +2,7 @@ import { ColumnDef, OnChangeFn, PaginationState, + Row, flexRender, getCoreRowModel, useReactTable @@ -16,6 +17,9 @@ import { TableRow } from '@/components/ui/table' import { DataTablePagination } from './pagination' +import { useCallback, useEffect, useRef, useState } from 'react' +import { cn } from '@/lib/utils' +import { FileSearch } from 'lucide-react' interface DataTableProps { columns: ColumnDef[] @@ -24,6 +28,9 @@ interface DataTableProps { pagination?: PaginationState rowCount?: number isLoading?: boolean + onRowClick?: (row: Row) => void + emptyMessage?: string + emptyDescription?: string } export function DataTable({ @@ -32,8 +39,14 @@ export function DataTable({ setPagination, pagination = { pageIndex: 0, pageSize: 10 }, rowCount = 1, - isLoading + isLoading, + onRowClick, + emptyMessage = 'No results found', + emptyDescription = 'Try adjusting your search or filters' }: DataTableProps) { + const [focusedRowIndex, setFocusedRowIndex] = useState(-1) + const tableContainerRef = useRef(null) + const table = useReactTable({ data, columns, @@ -48,13 +61,68 @@ export function DataTable({ rowCount }) + const rows = table.getRowModel().rows + + // Keyboard navigation handler + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (!rows.length) return + + // Only handle if table container or its children are focused + if (!tableContainerRef.current?.contains(document.activeElement)) return + + switch (e.key) { + case 'j': + case 'ArrowDown': + e.preventDefault() + setFocusedRowIndex((prev) => Math.min(prev + 1, rows.length - 1)) + break + case 'k': + case 'ArrowUp': + e.preventDefault() + setFocusedRowIndex((prev) => Math.max(prev - 1, 0)) + break + case 'Enter': + e.preventDefault() + if (focusedRowIndex >= 0 && focusedRowIndex < rows.length && onRowClick) { + onRowClick(rows[focusedRowIndex]) + } + break + case 'Escape': + e.preventDefault() + setFocusedRowIndex(-1) + break + } + }, + [rows, focusedRowIndex, onRowClick] + ) + + // Register keyboard event listener + useEffect(() => { + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [handleKeyDown]) + + // Reset focus when data changes + useEffect(() => { + setFocusedRowIndex(-1) + }, [data]) + + // Scroll focused row into view + useEffect(() => { + if (focusedRowIndex >= 0) { + const row = tableContainerRef.current?.querySelector(`[data-row-index="${focusedRowIndex}"]`) + row?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) + } + }, [focusedRowIndex]) + return ( -
    -
    +
    +
    - + {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { return ( @@ -68,9 +136,23 @@ export function DataTable({ ))} - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - + {rows?.length ? ( + rows.map((row, index) => ( + { + setFocusedRowIndex(index) + onRowClick?.(row) + }} + onMouseEnter={() => setFocusedRowIndex(index)} + > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -79,16 +161,22 @@ export function DataTable({ )) ) : ( - - - No results. + + +
    +
    + +
    +

    {emptyMessage}

    +

    {emptyDescription}

    +
    )}
    -
    +
    diff --git a/src/renderer/src/components/ui/dialog.tsx b/src/renderer/src/components/ui/dialog.tsx index 6305418..1994b1a 100644 --- a/src/renderer/src/components/ui/dialog.tsx +++ b/src/renderer/src/components/ui/dialog.tsx @@ -36,7 +36,7 @@ const DialogContent = React.forwardRef< { + orientation?: 'horizontal' | 'vertical' + decorative?: boolean +} + +const Separator = React.forwardRef( + ({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( +
    + ) +) +Separator.displayName = 'Separator' + +export { Separator } diff --git a/src/renderer/src/components/ui/tooltip.tsx b/src/renderer/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..e84aa62 --- /dev/null +++ b/src/renderer/src/components/ui/tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import * as TooltipPrimitive from '@radix-ui/react-tooltip' + +import { cn } from '@/lib/utils' + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/renderer/src/contexts/ThemeContext.tsx b/src/renderer/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..343f903 --- /dev/null +++ b/src/renderer/src/contexts/ThemeContext.tsx @@ -0,0 +1,206 @@ +import React, { createContext, useContext, useEffect, useState, useCallback } from 'react' + +export type ThemeId = + | 'default-light' + | 'default-dark' + | 'medical-light' + | 'medical-dark' + | 'classic-light' + | 'classic-dark' + | 'minimal-light' + | 'minimal-dark' + +export type ThemeBase = 'default' | 'medical' | 'classic' | 'minimal' +export type ThemeMode = 'light' | 'dark' + +export interface Theme { + id: ThemeId + name: string + base: ThemeBase + mode: ThemeMode + fontFamily: string + description: string +} + +export const themes: Theme[] = [ + { + id: 'default-light', + name: 'Default Light', + base: 'default', + mode: 'light', + fontFamily: 'Inter', + description: 'Clean and modern with blue accents' + }, + { + id: 'default-dark', + name: 'Default Dark', + base: 'default', + mode: 'dark', + fontFamily: 'Inter', + description: 'Dark blue-gray palette' + }, + { + id: 'medical-light', + name: 'Medical Light', + base: 'medical', + mode: 'light', + fontFamily: 'IBM Plex Sans', + description: 'Professional clinical appearance' + }, + { + id: 'medical-dark', + name: 'Medical Dark', + base: 'medical', + mode: 'dark', + fontFamily: 'IBM Plex Sans', + description: 'Deep blue-green dark mode' + }, + { + id: 'classic-light', + name: 'Classic Light', + base: 'classic', + mode: 'light', + fontFamily: 'Source Serif 4', + description: 'Warm paper-like background' + }, + { + id: 'classic-dark', + name: 'Classic Dark', + base: 'classic', + mode: 'dark', + fontFamily: 'Source Serif 4', + description: 'Warm dark tones' + }, + { + id: 'minimal-light', + name: 'Minimal Light', + base: 'minimal', + mode: 'light', + fontFamily: 'Space Grotesk', + description: 'Pure white, sharp contrasts' + }, + { + id: 'minimal-dark', + name: 'Minimal Dark', + base: 'minimal', + mode: 'dark', + fontFamily: 'Space Grotesk', + description: 'True black, high contrast' + } +] + +interface ThemeContextValue { + theme: Theme + themeId: ThemeId + setTheme: (themeId: ThemeId) => void + toggleMode: () => void + themes: Theme[] + isSystemDark: boolean +} + +const ThemeContext = createContext(undefined) + +const THEME_STORAGE_KEY = 'app_theme' + +function getSystemPrefersDark(): boolean { + if (typeof window !== 'undefined' && window.matchMedia) { + return window.matchMedia('(prefers-color-scheme: dark)').matches + } + return false +} + +function getStoredTheme(): ThemeId | null { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(THEME_STORAGE_KEY) + if (stored && themes.some((t) => t.id === stored)) { + return stored as ThemeId + } + } + return null +} + +function getDefaultTheme(systemDark: boolean): ThemeId { + return systemDark ? 'default-dark' : 'default-light' +} + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [isSystemDark, setIsSystemDark] = useState(getSystemPrefersDark) + const [themeId, setThemeId] = useState(() => { + const stored = getStoredTheme() + return stored || getDefaultTheme(getSystemPrefersDark()) + }) + + const theme = themes.find((t) => t.id === themeId) || themes[0] + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + + const handleChange = (e: MediaQueryListEvent) => { + setIsSystemDark(e.matches) + } + + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + }, []) + + // Apply theme to document + useEffect(() => { + document.documentElement.setAttribute('data-theme', themeId) + localStorage.setItem(THEME_STORAGE_KEY, themeId) + }, [themeId]) + + const setTheme = useCallback((newThemeId: ThemeId) => { + setThemeId(newThemeId) + }, []) + + const toggleMode = useCallback(() => { + const currentTheme = themes.find((t) => t.id === themeId) + if (currentTheme) { + const newMode = currentTheme.mode === 'light' ? 'dark' : 'light' + const newThemeId = `${currentTheme.base}-${newMode}` as ThemeId + setThemeId(newThemeId) + } + }, [themeId]) + + return ( + + {children} + + ) +} + +export function useTheme() { + const context = useContext(ThemeContext) + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} + +// Helper to get the opposite mode theme +export function getOppositeTheme(themeId: ThemeId): ThemeId { + const theme = themes.find((t) => t.id === themeId) + if (!theme) return 'default-light' + const newMode = theme.mode === 'light' ? 'dark' : 'light' + return `${theme.base}-${newMode}` as ThemeId +} + +// Helper to get themes grouped by base +export function getThemesByBase(): Record { + return { + default: themes.filter((t) => t.base === 'default'), + medical: themes.filter((t) => t.base === 'medical'), + classic: themes.filter((t) => t.base === 'classic'), + minimal: themes.filter((t) => t.base === 'minimal') + } +} diff --git a/src/renderer/src/hooks/useDebounce.ts b/src/renderer/src/hooks/useDebounce.ts new file mode 100644 index 0000000..aa770c4 --- /dev/null +++ b/src/renderer/src/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} diff --git a/src/renderer/src/hooks/useKeyboardShortcuts.ts b/src/renderer/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..06ced23 --- /dev/null +++ b/src/renderer/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,153 @@ +import { useCallback, useEffect, useRef } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' + +interface KeyboardShortcutsOptions { + onOpenCommandPalette?: () => void + onShowHelp?: () => void +} + +export function useKeyboardShortcuts({ onOpenCommandPalette, onShowHelp }: KeyboardShortcutsOptions) { + const navigate = useNavigate() + const location = useLocation() + const gKeyPressed = useRef(false) + const gKeyTimeout = useRef>() + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + // Skip if user is typing in an input + const target = e.target as HTMLElement + const isInput = + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable + + // Handle g+ navigation shortcuts (vim-style) + if (!isInput && !e.metaKey && !e.ctrlKey && !e.altKey) { + if (e.key === 'g') { + e.preventDefault() + gKeyPressed.current = true + // Clear after 500ms + if (gKeyTimeout.current) { + clearTimeout(gKeyTimeout.current) + } + gKeyTimeout.current = setTimeout(() => { + gKeyPressed.current = false + }, 500) + return + } + + if (gKeyPressed.current) { + gKeyPressed.current = false + if (gKeyTimeout.current) { + clearTimeout(gKeyTimeout.current) + } + + switch (e.key) { + case 'h': + e.preventDefault() + navigate('/') + return + case 'p': + e.preventDefault() + navigate('/patients') + return + case 's': + e.preventDefault() + navigate('/surgeries') + return + case 'd': + e.preventDefault() + navigate('/doctors') + return + case 't': + e.preventDefault() + navigate('/settings') + return + } + } + } + + // Modifier key shortcuts + const isMod = e.metaKey || e.ctrlKey + + // ⌘K - Command palette (handled in MainLayout, but this is a fallback) + if (isMod && e.key === 'k') { + e.preventDefault() + onOpenCommandPalette?.() + return + } + + // ⌘/ - Show keyboard shortcuts help + if (isMod && e.key === '/') { + e.preventDefault() + onShowHelp?.() + return + } + + // ⌘N - New item (context-aware) + if (isMod && e.key === 'n' && !isInput) { + e.preventDefault() + // Context-aware new item + if (location.pathname.startsWith('/patients')) { + navigate('/patients/add') + } else if (location.pathname.startsWith('/surgeries')) { + navigate('/surgeries/add') + } else if (location.pathname.startsWith('/doctors')) { + navigate('/doctors/add') + } else { + // Default to new patient + navigate('/patients/add') + } + return + } + + // Escape - Navigate back or close modals + if (e.key === 'Escape' && !isInput) { + // This could trigger going back + // window.history.back() + } + }, + [navigate, location.pathname, onOpenCommandPalette, onShowHelp] + ) + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + if (gKeyTimeout.current) { + clearTimeout(gKeyTimeout.current) + } + } + }, [handleKeyDown]) +} + +// Keyboard shortcuts definitions for the help modal +export const keyboardShortcuts = [ + { + category: 'General', + shortcuts: [ + { keys: ['⌘', 'K'], description: 'Open command palette' }, + { keys: ['⌘', '/'], description: 'Show keyboard shortcuts' }, + { keys: ['⌘', 'N'], description: 'New item (context-aware)' }, + { keys: ['Esc'], description: 'Close dialog / Cancel' } + ] + }, + { + category: 'Navigation (press g then...)', + shortcuts: [ + { keys: ['g', 'h'], description: 'Go to Dashboard' }, + { keys: ['g', 'p'], description: 'Go to Patients' }, + { keys: ['g', 's'], description: 'Go to Surgeries' }, + { keys: ['g', 'd'], description: 'Go to Doctors' }, + { keys: ['g', 't'], description: 'Go to Settings' } + ] + }, + { + category: 'Tables', + shortcuts: [ + { keys: ['↑', '/', 'k'], description: 'Move up' }, + { keys: ['↓', '/', 'j'], description: 'Move down' }, + { keys: ['Enter'], description: 'Open selected item' } + ] + } +] diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index a4130f8..544ecd4 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -1,82 +1,577 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +/* Custom dark mode variant for data-theme attribute - matches all dark theme variants */ +@custom-variant dark (&:where([data-theme=dark], [data-theme=default-dark], [data-theme=medical-dark], [data-theme=classic-dark], [data-theme=minimal-dark], [data-theme=dark] *, [data-theme=default-dark] *, [data-theme=medical-dark] *, [data-theme=classic-dark] *, [data-theme=minimal-dark] *)); -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 0.5rem; - } - - - :root[data-theme='dark'] { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; +/* Source paths for class detection - relative to this CSS file */ +@source "."; +@source "../../../node_modules/@radix-ui"; + +/* Typography plugin */ +@plugin '@tailwindcss/typography'; + +/* Custom theme configuration */ +@theme { + /* Font families - will be extended by theme variants */ + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; + + /* Border radius */ + --radius-lg: 0.5rem; + --radius-md: calc(0.5rem - 2px); + --radius-sm: calc(0.5rem - 4px); + + /* Container settings */ + --container-center: true; + --container-padding: 2rem; + + /* Custom animations */ + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + --animate-shine: shine 8s ease-in-out infinite; + --animate-fade-in: fade-in 0.2s ease-out; + --animate-slide-in-from-top: slide-in-from-top 0.2s ease-out; + --animate-slide-in-from-bottom: slide-in-from-bottom 0.2s ease-out; + --animate-slide-in-from-left: slide-in-from-left 0.2s ease-out; + --animate-slide-in-from-right: slide-in-from-right 0.2s ease-out; + --animate-zoom-in: zoom-in 0.2s ease-out; + --animate-spin-slow: spin 3s linear infinite; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } + + @keyframes shine { + from { + background-position: 200% 0; + } + to { + background-position: -200% 0; + } + } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes slide-in-from-top { + from { + transform: translateY(-100%); + } + to { + transform: translateY(0); + } + } + + @keyframes slide-in-from-bottom { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } + } + + @keyframes slide-in-from-left { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } + } + + @keyframes slide-in-from-right { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } + } + + @keyframes zoom-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } } } +/* Base semantic colors using CSS custom properties */ +/* These will be overridden by theme variants */ +:root { + /* Light theme defaults */ + --background: oklch(100% 0 0); + --foreground: oklch(14.1% 0.02 264.7); + + --card: oklch(100% 0 0); + --card-foreground: oklch(14.1% 0.02 264.7); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(14.1% 0.02 264.7); + + --primary: oklch(54.6% 0.245 262.9); + --primary-foreground: oklch(97.1% 0.01 264.7); + + --secondary: oklch(96.7% 0.01 264.7); + --secondary-foreground: oklch(21% 0.02 264.7); + + --muted: oklch(96.7% 0.01 264.7); + --muted-foreground: oklch(55.6% 0.02 264.7); + + --accent: oklch(96.7% 0.01 264.7); + --accent-foreground: oklch(21% 0.02 264.7); + + --destructive: oklch(57.7% 0.215 27.3); + --destructive-foreground: oklch(97.1% 0.01 264.7); + + --border: oklch(91.4% 0.01 264.7); + --input: oklch(91.4% 0.01 264.7); + --ring: oklch(54.6% 0.245 262.9); + + --chart-1: oklch(62.8% 0.19 259); + --chart-2: oklch(63.2% 0.172 165); + --chart-3: oklch(47% 0.12 264.7); + --chart-4: oklch(75.9% 0.154 89); + --chart-5: oklch(66.9% 0.202 41); + + /* Sidebar colors */ + --sidebar-background: oklch(98.5% 0.005 264.7); + --sidebar-foreground: oklch(37% 0.02 264.7); + --sidebar-primary: oklch(54.6% 0.245 262.9); + --sidebar-primary-foreground: oklch(97.1% 0.01 264.7); + --sidebar-accent: oklch(95% 0.01 264.7); + --sidebar-accent-foreground: oklch(21% 0.02 264.7); + --sidebar-border: oklch(91.4% 0.01 264.7); + --sidebar-ring: oklch(54.6% 0.245 262.9); +} + +/* Dark theme */ +:root[data-theme='dark'], +[data-theme='default-dark'] { + --background: oklch(14.5% 0.035 264.7); + --foreground: oklch(97.1% 0.01 264.7); + + --card: oklch(14.5% 0.035 264.7); + --card-foreground: oklch(97.1% 0.01 264.7); + + --popover: oklch(14.5% 0.035 264.7); + --popover-foreground: oklch(97.1% 0.01 264.7); + + --primary: oklch(62.8% 0.19 259); + --primary-foreground: oklch(21% 0.02 264.7); + + --secondary: oklch(25% 0.04 264.7); + --secondary-foreground: oklch(97.1% 0.01 264.7); + + --muted: oklch(25% 0.04 264.7); + --muted-foreground: oklch(71.1% 0.02 264.7); + + --accent: oklch(25% 0.04 264.7); + --accent-foreground: oklch(97.1% 0.01 264.7); + + --destructive: oklch(45% 0.18 27.3); + --destructive-foreground: oklch(97.1% 0.01 264.7); + + --border: oklch(25% 0.04 264.7); + --input: oklch(25% 0.04 264.7); + --ring: oklch(56% 0.2 259); + + --chart-1: oklch(62.8% 0.19 259); + --chart-2: oklch(63.2% 0.172 165); + --chart-3: oklch(72% 0.15 264.7); + --chart-4: oklch(75.9% 0.154 89); + --chart-5: oklch(66.9% 0.202 41); + + /* Sidebar colors - dark */ + --sidebar-background: oklch(17% 0.04 264.7); + --sidebar-foreground: oklch(75% 0.02 264.7); + --sidebar-primary: oklch(62.8% 0.19 259); + --sidebar-primary-foreground: oklch(21% 0.02 264.7); + --sidebar-accent: oklch(22% 0.035 264.7); + --sidebar-accent-foreground: oklch(97.1% 0.01 264.7); + --sidebar-border: oklch(25% 0.04 264.7); + --sidebar-ring: oklch(56% 0.2 259); +} + +/* Extend theme with semantic color utilities */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar-background: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +/* Animation utilities for Shadcn components */ +@utility animate-in { + animation-name: enter; + animation-duration: 150ms; + --tw-enter-opacity: initial; + --tw-enter-scale: initial; + --tw-enter-rotate: initial; + --tw-enter-translate-x: initial; + --tw-enter-translate-y: initial; +} + +@utility animate-out { + animation-name: exit; + animation-duration: 150ms; + --tw-exit-opacity: initial; + --tw-exit-scale: initial; + --tw-exit-rotate: initial; + --tw-exit-translate-x: initial; + --tw-exit-translate-y: initial; +} + +@utility fade-in-0 { + --tw-enter-opacity: 0; +} + +@utility fade-in-100 { + --tw-enter-opacity: 1; +} + +@utility fade-out-0 { + --tw-exit-opacity: 0; +} + +@utility fade-out-100 { + --tw-exit-opacity: 1; +} + +@utility zoom-in-50 { + --tw-enter-scale: 0.5; +} + +@utility zoom-in-75 { + --tw-enter-scale: 0.75; +} + +@utility zoom-in-90 { + --tw-enter-scale: 0.9; +} + +@utility zoom-in-95 { + --tw-enter-scale: 0.95; +} + +@utility zoom-in-100 { + --tw-enter-scale: 1; +} + +@utility zoom-out-50 { + --tw-exit-scale: 0.5; +} + +@utility zoom-out-75 { + --tw-exit-scale: 0.75; +} + +@utility zoom-out-90 { + --tw-exit-scale: 0.9; +} + +@utility zoom-out-95 { + --tw-exit-scale: 0.95; +} + +@utility zoom-out-100 { + --tw-exit-scale: 1; +} + +@utility slide-in-from-top-2 { + --tw-enter-translate-y: -0.5rem; +} + +@utility slide-in-from-top-4 { + --tw-enter-translate-y: -1rem; +} + +@utility slide-in-from-top-full { + --tw-enter-translate-y: -100%; +} + +@utility slide-in-from-bottom-2 { + --tw-enter-translate-y: 0.5rem; +} + +@utility slide-in-from-bottom-4 { + --tw-enter-translate-y: 1rem; +} + +@utility slide-in-from-bottom-full { + --tw-enter-translate-y: 100%; +} + +@utility slide-in-from-left-0 { + --tw-enter-translate-x: 0; +} + +@utility slide-in-from-left-2 { + --tw-enter-translate-x: -0.5rem; +} + +@utility slide-in-from-left-4 { + --tw-enter-translate-x: -1rem; +} + +@utility slide-in-from-left-full { + --tw-enter-translate-x: -100%; +} + +@utility slide-in-from-right-2 { + --tw-enter-translate-x: 0.5rem; +} + +@utility slide-in-from-right-4 { + --tw-enter-translate-x: 1rem; +} + +@utility slide-in-from-right-full { + --tw-enter-translate-x: 100%; +} + +@utility slide-out-to-top-2 { + --tw-exit-translate-y: -0.5rem; +} + +@utility slide-out-to-top-full { + --tw-exit-translate-y: -100%; +} + +@utility slide-out-to-bottom-2 { + --tw-exit-translate-y: 0.5rem; +} + +@utility slide-out-to-bottom-full { + --tw-exit-translate-y: 100%; +} + +@utility slide-out-to-left-0 { + --tw-exit-translate-x: 0; +} + +@utility slide-out-to-left-2 { + --tw-exit-translate-x: -0.5rem; +} + +@utility slide-out-to-left-full { + --tw-exit-translate-x: -100%; +} + +@utility slide-out-to-left-half { + --tw-exit-translate-x: -50%; +} + +@utility slide-out-to-right-2 { + --tw-exit-translate-x: 0.5rem; +} + +@utility slide-out-to-right-full { + --tw-exit-translate-x: 100%; +} + +@utility slide-in-from-top-48 { + --tw-enter-translate-y: -48%; +} + +@utility slide-out-to-top-48 { + --tw-exit-translate-y: -48%; +} + +@utility slide-in-from-left-half { + --tw-enter-translate-x: -50%; +} + +@utility duration-200 { + animation-duration: 200ms; +} + +@utility duration-300 { + animation-duration: 300ms; +} + +@keyframes enter { + from { + opacity: var(--tw-enter-opacity, 1); + transform: translate3d( + var(--tw-enter-translate-x, 0), + var(--tw-enter-translate-y, 0), + 0 + ) + scale3d( + var(--tw-enter-scale, 1), + var(--tw-enter-scale, 1), + var(--tw-enter-scale, 1) + ) + rotate(var(--tw-enter-rotate, 0)); + } +} + +@keyframes exit { + to { + opacity: var(--tw-exit-opacity, 1); + transform: translate3d( + var(--tw-exit-translate-x, 0), + var(--tw-exit-translate-y, 0), + 0 + ) + scale3d( + var(--tw-exit-scale, 1), + var(--tw-exit-scale, 1), + var(--tw-exit-scale, 1) + ) + rotate(var(--tw-exit-rotate, 0)); + } +} + +/* Sidebar utilities - manually defined because @theme inline with var() doesn't generate all utilities */ +@layer utilities { + /* Background colors */ + .bg-sidebar-background { + background-color: var(--sidebar-background); + } + .bg-sidebar-primary { + background-color: var(--sidebar-primary); + } + .bg-sidebar-accent { + background-color: var(--sidebar-accent); + } + + /* Text colors */ + .text-sidebar-foreground { + color: var(--sidebar-foreground); + } + .text-sidebar-primary { + color: var(--sidebar-primary); + } + .text-sidebar-primary-foreground { + color: var(--sidebar-primary-foreground); + } + .text-sidebar-accent-foreground { + color: var(--sidebar-accent-foreground); + } + + /* Border colors */ + .border-sidebar-border { + border-color: var(--sidebar-border); + } + + /* Opacity variants using color-mix (required because /opacity doesn't work with var()) */ + .text-sidebar-foreground\/70 { + color: color-mix(in oklch, var(--sidebar-foreground) 70%, transparent); + } + .text-sidebar-foreground\/60 { + color: color-mix(in oklch, var(--sidebar-foreground) 60%, transparent); + } + .bg-sidebar-accent\/50 { + background-color: color-mix(in oklch, var(--sidebar-accent) 50%, transparent); + } + .bg-sidebar-primary\/10 { + background-color: color-mix(in oklch, var(--sidebar-primary) 10%, transparent); + } + + /* Hover variants */ + .hover\:bg-sidebar-accent:hover { + background-color: var(--sidebar-accent); + } + .hover\:bg-sidebar-accent\/50:hover { + background-color: color-mix(in oklch, var(--sidebar-accent) 50%, transparent); + } + .hover\:text-sidebar-foreground:hover { + color: var(--sidebar-foreground); + } + .hover\:text-sidebar-accent-foreground:hover { + color: var(--sidebar-accent-foreground); + } + + /* Group hover variants */ + .group:hover .group-hover\:text-sidebar-foreground { + color: var(--sidebar-foreground); + } +} + +/* Base layer */ @layer base { * { @apply border-border; } + body { @apply bg-background text-foreground; + font-family: var(--font-sans); } /* Scrollbar */ ::-webkit-scrollbar { width: 0.5rem; - background-color: hsl(var(--border) / 50%); - @apply rounded-sm; + background-color: color-mix(in oklch, var(--border) 50%, transparent); + border-radius: 0.125rem; } ::-webkit-scrollbar-thumb { - background-color: hsl(var(--border) / 75%); - @apply rounded-sm; + background-color: color-mix(in oklch, var(--border) 75%, transparent); + border-radius: 0.125rem; } ::-webkit-scrollbar-thumb:hover { - background-color: hsl(from hsl(var(--border)) calc(h + 5) s l); + background-color: var(--border); } + /* TipTap placeholder */ .tiptap p.is-editor-empty:first-child::before { - color: #adb5bd; + color: var(--muted-foreground); content: attr(data-placeholder); float: left; height: 0; @@ -85,9 +580,9 @@ /* Template preview styles */ .template-preview { - font-family: system-ui, -apple-system, sans-serif; + font-family: var(--font-sans); line-height: 1.6; - color: hsl(var(--foreground)); + color: var(--foreground); } .template-preview h1, @@ -96,18 +591,30 @@ .template-preview h4, .template-preview h5, .template-preview h6 { - color: hsl(var(--foreground)); + color: var(--foreground); margin-top: 1em; margin-bottom: 0.5em; font-weight: 600; } - .template-preview h1 { font-size: 1.75rem; } - .template-preview h2 { font-size: 1.5rem; } - .template-preview h3 { font-size: 1.25rem; } - .template-preview h4 { font-size: 1.125rem; } - .template-preview h5 { font-size: 1rem; } - .template-preview h6 { font-size: 0.875rem; } + .template-preview h1 { + font-size: 1.75rem; + } + .template-preview h2 { + font-size: 1.5rem; + } + .template-preview h3 { + font-size: 1.25rem; + } + .template-preview h4 { + font-size: 1.125rem; + } + .template-preview h5 { + font-size: 1rem; + } + .template-preview h6 { + font-size: 0.875rem; + } .template-preview p { margin-top: 0.5em; @@ -133,10 +640,10 @@ } .template-preview blockquote { - border-left: 3px solid hsl(var(--border)); + border-left: 3px solid var(--border); padding-left: 1em; margin: 0.5em 0; - color: hsl(var(--muted-foreground)); + color: var(--muted-foreground); font-style: italic; } @@ -151,19 +658,19 @@ } .template-preview a { - color: hsl(var(--primary)); + color: var(--primary); text-decoration: underline; } .template-preview code { - background-color: hsl(var(--muted)); + background-color: var(--muted); padding: 0.125em 0.25em; border-radius: 0.25em; font-size: 0.875em; } .template-preview pre { - background-color: hsl(var(--muted)); + background-color: var(--muted); padding: 0.75em; border-radius: 0.375em; overflow-x: auto; @@ -177,12 +684,12 @@ .template-preview th, .template-preview td { - border: 1px solid hsl(var(--border)); + border: 1px solid var(--border); padding: 0.5em; } .template-preview th { - background-color: hsl(var(--muted)); + background-color: var(--muted); font-weight: 600; } } diff --git a/src/renderer/src/lib/queries.ts b/src/renderer/src/lib/queries.ts index aa88c49..6d8a1cd 100644 --- a/src/renderer/src/lib/queries.ts +++ b/src/renderer/src/lib/queries.ts @@ -84,5 +84,15 @@ export const queries = createQueryKeyStore({ queryKey: null, queryFn: () => unwrapResult(window.api.invoke('listBackups')) } + }, + dashboard: { + stats: { + queryKey: null, + queryFn: () => unwrapResult(window.api.invoke('getDashboardStats')) + }, + recentActivity: (limit: number = 10) => ({ + queryKey: [limit], + queryFn: () => unwrapResult(window.api.invoke('getRecentActivity', limit)) + }) } }) diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index c14023f..8be5d05 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,3 +1,5 @@ +import './styles/fonts.css' +import './styles/themes.css' import './index.css' import React from 'react' diff --git a/src/renderer/src/routes/home.tsx b/src/renderer/src/routes/home.tsx index fbd98a9..dd885a8 100644 --- a/src/renderer/src/routes/home.tsx +++ b/src/renderer/src/routes/home.tsx @@ -1,77 +1,278 @@ -import { Button } from '@renderer/components/ui/button' -import { Input } from '@renderer/components/ui/input' -import hero from '../assets/hero.svg?asset' -import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { queries } from '@renderer/lib/queries' import { useNavigate } from 'react-router-dom' -import { Hospital, Plus } from 'lucide-react' import { useSettings } from '@renderer/contexts/SettingsContext' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@renderer/components/ui/card' +import { Button } from '@renderer/components/ui/button' +import { Skeleton } from '@renderer/components/ui/skeleton' +import { + Users, + Stethoscope, + UserCog, + Plus, + TrendingUp, + Calendar, + ArrowRight, + Activity +} from 'lucide-react' +import { formatDate } from '@renderer/lib/utils' + +interface StatCardProps { + title: string + value: number | string + description?: string + icon: React.ReactNode + trend?: string + isLoading?: boolean +} + +function StatCard({ title, value, description, icon, trend, isLoading }: StatCardProps) { + return ( + + + {title} +
    + {icon} +
    +
    + + {isLoading ? ( + <> + + + + ) : ( + <> +
    {value}
    + {description && ( +

    + {trend && } + {description} +

    + )} + + )} +
    +
    + ) +} + +interface QuickActionProps { + title: string + description: string + icon: React.ReactNode + onClick: () => void +} + +function QuickAction({ title, description, icon, onClick }: QuickActionProps) { + return ( + + ) +} + +interface RecentActivityItem { + id: number + type: 'patient' | 'surgery' + title: string + subtitle: string + date: string +} + +function RecentActivityList({ + items, + isLoading +}: { + items: RecentActivityItem[] + isLoading: boolean +}) { + const navigate = useNavigate() + + if (isLoading) { + return ( +
    + {[1, 2, 3, 4, 5].map((i) => ( +
    + +
    + + +
    + +
    + ))} +
    + ) + } -const SearchIcon = () => ( - - - -) + if (items.length === 0) { + return ( +
    + +

    No recent activity

    +
    + ) + } + + return ( +
    + {items.map((item) => ( + + ))} +
    + ) +} export default function Home() { const navigate = useNavigate() - const [search, setSearch] = useState('') const { settings } = useSettings() - const handleSearch = () => { - navigate(`/patients?q=${search}`) - } + const { data: stats, isLoading: statsLoading } = useQuery({ + ...queries.dashboard.stats, + refetchInterval: 30000 // Refresh every 30 seconds + }) + + const { data: recentActivity, isLoading: activityLoading } = useQuery({ + ...queries.dashboard.recentActivity(10) + }) return ( -
    -

    OpNotes

    - {settings && ( -
    -
    {settings['hospital']}
    -
    {settings['unit']}
    -
    - )} - hero -
    - setSearch(e.target.value)} +
    + {/* Welcome header */} +
    +

    + Welcome to Op Notes +

    + {settings?.hospital && ( +

    + {settings.hospital} + {settings.unit && ` - ${settings.unit}`} +

    + )} +
    + + {/* Stats cards */} +
    + } + trend={stats?.patientsThisMonth ? '+' : undefined} + isLoading={statsLoading} + /> + } + trend={stats?.surgeriesThisMonth ? '+' : undefined} + isLoading={statsLoading} + /> + } + isLoading={statsLoading} + /> + } + isLoading={statsLoading} /> -
    -
    - - + +
    + {/* Quick Actions */} + + + Quick Actions + Common tasks you can perform + + + } + onClick={() => navigate('/patients/add')} + /> + } + onClick={() => navigate('/patients')} + /> + } + onClick={() => navigate('/surgeries')} + /> + } + onClick={() => navigate('/doctors')} + /> + + + + {/* Recent Activity */} + + + Recent Activity + Latest patients and surgeries + + + + +
    ) diff --git a/src/renderer/src/routes/root.tsx b/src/renderer/src/routes/root.tsx index 045b6eb..c67a580 100644 --- a/src/renderer/src/routes/root.tsx +++ b/src/renderer/src/routes/root.tsx @@ -1,43 +1,17 @@ -import { NavLink, Outlet } from 'react-router-dom' import { Toaster } from 'react-hot-toast' import { cn } from '@renderer/lib/utils' import { useMutation, useQuery } from '@tanstack/react-query' import { queries } from '@renderer/lib/queries' import hero from '../assets/hero.svg?asset' -import { Loader2Icon, Settings } from 'lucide-react' +import { Loader2Icon } from 'lucide-react' import { useEffect } from 'react' import { SettingsProvider, useSettings } from '@renderer/contexts/SettingsContext' import { UpdateProvider } from '@renderer/contexts/UpdateContext' -import { UpdateIndicator } from '@renderer/components/update/UpdateIndicator' +import { ThemeProvider } from '@renderer/contexts/ThemeContext' import { PoweredBy } from '@renderer/components/brand/PowredBy' -import { MadeWithLove } from '@renderer/components/brand/MadeWithLove' import { OnboardingWizard } from '@renderer/components/onboarding/OnboardingWizard' - -const NavLinkComponent = ({ - to, - children, - className -}: { - to: string - children: React.ReactNode - className?: string -}) => { - return ( - - cn( - 'text-secondary-foreground text-sm underline-offset-4 hover:underline cursor-pointer hover:bg-accent/80 p-2 rounded-lg', - (isActive || isPending) && 'bg-accent', - className - ) - } - > - {children} - - ) -} +import { MainLayout } from '@renderer/components/layout/MainLayout' const SplashScreen = ({ error }: { error?: string }) => { const { data: appVersion, isLoading: isLoadingAppVersion } = useQuery({ @@ -91,42 +65,15 @@ const OnboardingGate = ({ children }: { children: React.ReactNode }) => { return <>{children} } -const Providers = ({ children }) => { - return ( - - - {children} - - - ) -} - -const MainLayout = () => { - const { appVersion } = useSettings() - +const Providers = ({ children }: { children: React.ReactNode }) => { return ( - <> - -
    - -
    -
    - -
    - + + + + {children} + + + ) } @@ -155,7 +102,7 @@ export default function Root() { return ( <> -
    +
    {isBooting && } {!isBooting && isSuccess && ( diff --git a/src/renderer/src/styles/fonts.css b/src/renderer/src/styles/fonts.css new file mode 100644 index 0000000..4d64c9b --- /dev/null +++ b/src/renderer/src/styles/fonts.css @@ -0,0 +1,88 @@ +/* Inter - Default theme font */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* IBM Plex Sans - Medical Professional theme font */ +@font-face { + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYXgKVElMYYaJe8bpLHnCwDKhdHeFaxOedc.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYX9KVElMYYaJe8bpLHnCwDKjSL9AIFsdP3pBms.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYX9KVElMYYaJe8bpLHnCwDKjQ76AIFsdP3pBms.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYX9KVElMYYaJe8bpLHnCwDKjWr7AIFsdP3pBms.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Source Serif Pro - Classic theme font */ +@font-face { + font-family: 'Source Serif 4'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url('https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Space Grotesk - Minimal theme font */ +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 300 700; + font-display: swap; + src: url('https://fonts.gstatic.com/s/spacegrotesk/v16/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj7oUXskPMBBSSJLm2E.woff2') + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* Font family variables for themes */ +:root { + --font-inter: 'Inter', ui-sans-serif, system-ui, sans-serif; + --font-ibm-plex: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; + --font-source-serif: 'Source Serif 4', ui-serif, Georgia, serif; + --font-space-grotesk: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif; +} diff --git a/src/renderer/src/styles/themes.css b/src/renderer/src/styles/themes.css new file mode 100644 index 0000000..ebf27c3 --- /dev/null +++ b/src/renderer/src/styles/themes.css @@ -0,0 +1,346 @@ +/* ============================================ + Theme Variants - 4 themes x 2 modes = 8 total + ============================================ */ + +/* Default Theme - Light (Inter font) */ +[data-theme='default-light'], +:root:not([data-theme]) { + --font-family: var(--font-inter); + + --background: oklch(100% 0 0); + --foreground: oklch(14.1% 0.02 264.7); + + --card: oklch(100% 0 0); + --card-foreground: oklch(14.1% 0.02 264.7); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(14.1% 0.02 264.7); + + --primary: oklch(54.6% 0.245 262.9); + --primary-foreground: oklch(97.1% 0.01 264.7); + + --secondary: oklch(96.7% 0.01 264.7); + --secondary-foreground: oklch(21% 0.02 264.7); + + --muted: oklch(96.7% 0.01 264.7); + --muted-foreground: oklch(55.6% 0.02 264.7); + + --accent: oklch(96.7% 0.01 264.7); + --accent-foreground: oklch(21% 0.02 264.7); + + --destructive: oklch(57.7% 0.215 27.3); + --destructive-foreground: oklch(97.1% 0.01 264.7); + + --border: oklch(91.4% 0.01 264.7); + --input: oklch(91.4% 0.01 264.7); + --ring: oklch(54.6% 0.245 262.9); + + --sidebar-background: oklch(98.5% 0.005 264.7); + --sidebar-foreground: oklch(37% 0.02 264.7); + --sidebar-primary: oklch(54.6% 0.245 262.9); + --sidebar-primary-foreground: oklch(97.1% 0.01 264.7); + --sidebar-accent: oklch(95% 0.01 264.7); + --sidebar-accent-foreground: oklch(21% 0.02 264.7); + --sidebar-border: oklch(91.4% 0.01 264.7); + --sidebar-ring: oklch(54.6% 0.245 262.9); +} + +/* Default Theme - Dark (Inter font) */ +[data-theme='default-dark'] { + --font-family: var(--font-inter); + + --background: oklch(14.5% 0.035 264.7); + --foreground: oklch(97.1% 0.01 264.7); + + --card: oklch(17% 0.035 264.7); + --card-foreground: oklch(97.1% 0.01 264.7); + + --popover: oklch(17% 0.035 264.7); + --popover-foreground: oklch(97.1% 0.01 264.7); + + --primary: oklch(62.8% 0.19 259); + --primary-foreground: oklch(14% 0.02 264.7); + + --secondary: oklch(25% 0.04 264.7); + --secondary-foreground: oklch(97.1% 0.01 264.7); + + --muted: oklch(25% 0.04 264.7); + --muted-foreground: oklch(71.1% 0.02 264.7); + + --accent: oklch(25% 0.04 264.7); + --accent-foreground: oklch(97.1% 0.01 264.7); + + --destructive: oklch(55% 0.2 27.3); + --destructive-foreground: oklch(97.1% 0.01 264.7); + + --border: oklch(28% 0.04 264.7); + --input: oklch(28% 0.04 264.7); + --ring: oklch(56% 0.2 259); + + --sidebar-background: oklch(12% 0.035 264.7); + --sidebar-foreground: oklch(75% 0.02 264.7); + --sidebar-primary: oklch(62.8% 0.19 259); + --sidebar-primary-foreground: oklch(14% 0.02 264.7); + --sidebar-accent: oklch(22% 0.035 264.7); + --sidebar-accent-foreground: oklch(97.1% 0.01 264.7); + --sidebar-border: oklch(25% 0.04 264.7); + --sidebar-ring: oklch(56% 0.2 259); +} + +/* Medical Professional Theme - Light (IBM Plex Sans) */ +[data-theme='medical-light'] { + --font-family: var(--font-ibm-plex); + + --background: oklch(99.5% 0.005 220); + --foreground: oklch(20% 0.02 220); + + --card: oklch(100% 0 0); + --card-foreground: oklch(20% 0.02 220); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(20% 0.02 220); + + --primary: oklch(48% 0.15 220); + --primary-foreground: oklch(98% 0.01 220); + + --secondary: oklch(96% 0.015 220); + --secondary-foreground: oklch(25% 0.02 220); + + --muted: oklch(96% 0.015 220); + --muted-foreground: oklch(50% 0.02 220); + + --accent: oklch(92% 0.03 180); + --accent-foreground: oklch(25% 0.02 180); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(90% 0.015 220); + --input: oklch(90% 0.015 220); + --ring: oklch(48% 0.15 220); + + --sidebar-background: oklch(97% 0.01 220); + --sidebar-foreground: oklch(35% 0.02 220); + --sidebar-primary: oklch(48% 0.15 220); + --sidebar-primary-foreground: oklch(98% 0.01 220); + --sidebar-accent: oklch(94% 0.02 220); + --sidebar-accent-foreground: oklch(25% 0.02 220); + --sidebar-border: oklch(90% 0.015 220); + --sidebar-ring: oklch(48% 0.15 220); +} + +/* Medical Professional Theme - Dark (IBM Plex Sans) */ +[data-theme='medical-dark'] { + --font-family: var(--font-ibm-plex); + + --background: oklch(15% 0.03 220); + --foreground: oklch(96% 0.01 220); + + --card: oklch(18% 0.03 220); + --card-foreground: oklch(96% 0.01 220); + + --popover: oklch(18% 0.03 220); + --popover-foreground: oklch(96% 0.01 220); + + --primary: oklch(60% 0.15 200); + --primary-foreground: oklch(15% 0.02 200); + + --secondary: oklch(25% 0.035 220); + --secondary-foreground: oklch(96% 0.01 220); + + --muted: oklch(25% 0.035 220); + --muted-foreground: oklch(70% 0.02 220); + + --accent: oklch(30% 0.05 180); + --accent-foreground: oklch(96% 0.01 180); + + --destructive: oklch(50% 0.18 25); + --destructive-foreground: oklch(96% 0.01 25); + + --border: oklch(28% 0.035 220); + --input: oklch(28% 0.035 220); + --ring: oklch(60% 0.15 200); + + --sidebar-background: oklch(12% 0.03 220); + --sidebar-foreground: oklch(75% 0.02 220); + --sidebar-primary: oklch(60% 0.15 200); + --sidebar-primary-foreground: oklch(15% 0.02 200); + --sidebar-accent: oklch(22% 0.03 220); + --sidebar-accent-foreground: oklch(96% 0.01 220); + --sidebar-border: oklch(25% 0.035 220); + --sidebar-ring: oklch(60% 0.15 200); +} + +/* Classic Theme - Light (Source Serif Pro) */ +[data-theme='classic-light'] { + --font-family: var(--font-source-serif); + + --background: oklch(98% 0.01 80); + --foreground: oklch(22% 0.02 60); + + --card: oklch(99% 0.008 80); + --card-foreground: oklch(22% 0.02 60); + + --popover: oklch(99% 0.008 80); + --popover-foreground: oklch(22% 0.02 60); + + --primary: oklch(45% 0.1 40); + --primary-foreground: oklch(97% 0.01 40); + + --secondary: oklch(95% 0.015 80); + --secondary-foreground: oklch(28% 0.02 60); + + --muted: oklch(95% 0.015 80); + --muted-foreground: oklch(50% 0.02 60); + + --accent: oklch(93% 0.02 80); + --accent-foreground: oklch(28% 0.02 60); + + --destructive: oklch(52% 0.2 25); + --destructive-foreground: oklch(97% 0.01 25); + + --border: oklch(90% 0.015 80); + --input: oklch(90% 0.015 80); + --ring: oklch(45% 0.1 40); + + --sidebar-background: oklch(96% 0.012 80); + --sidebar-foreground: oklch(35% 0.02 60); + --sidebar-primary: oklch(45% 0.1 40); + --sidebar-primary-foreground: oklch(97% 0.01 40); + --sidebar-accent: oklch(93% 0.018 80); + --sidebar-accent-foreground: oklch(28% 0.02 60); + --sidebar-border: oklch(88% 0.015 80); + --sidebar-ring: oklch(45% 0.1 40); +} + +/* Classic Theme - Dark (Source Serif Pro) */ +[data-theme='classic-dark'] { + --font-family: var(--font-source-serif); + + --background: oklch(18% 0.02 60); + --foreground: oklch(94% 0.01 80); + + --card: oklch(21% 0.02 60); + --card-foreground: oklch(94% 0.01 80); + + --popover: oklch(21% 0.02 60); + --popover-foreground: oklch(94% 0.01 80); + + --primary: oklch(65% 0.12 45); + --primary-foreground: oklch(18% 0.02 45); + + --secondary: oklch(28% 0.025 60); + --secondary-foreground: oklch(94% 0.01 80); + + --muted: oklch(28% 0.025 60); + --muted-foreground: oklch(68% 0.02 80); + + --accent: oklch(32% 0.03 60); + --accent-foreground: oklch(94% 0.01 80); + + --destructive: oklch(48% 0.17 25); + --destructive-foreground: oklch(94% 0.01 25); + + --border: oklch(30% 0.025 60); + --input: oklch(30% 0.025 60); + --ring: oklch(65% 0.12 45); + + --sidebar-background: oklch(15% 0.02 60); + --sidebar-foreground: oklch(72% 0.02 80); + --sidebar-primary: oklch(65% 0.12 45); + --sidebar-primary-foreground: oklch(18% 0.02 45); + --sidebar-accent: oklch(25% 0.025 60); + --sidebar-accent-foreground: oklch(94% 0.01 80); + --sidebar-border: oklch(28% 0.025 60); + --sidebar-ring: oklch(65% 0.12 45); +} + +/* Minimal Theme - Light (Space Grotesk) */ +[data-theme='minimal-light'] { + --font-family: var(--font-space-grotesk); + + --background: oklch(100% 0 0); + --foreground: oklch(15% 0 0); + + --card: oklch(100% 0 0); + --card-foreground: oklch(15% 0 0); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(15% 0 0); + + --primary: oklch(25% 0 0); + --primary-foreground: oklch(100% 0 0); + + --secondary: oklch(96% 0 0); + --secondary-foreground: oklch(20% 0 0); + + --muted: oklch(96% 0 0); + --muted-foreground: oklch(50% 0 0); + + --accent: oklch(96% 0 0); + --accent-foreground: oklch(20% 0 0); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(100% 0 0); + + --border: oklch(88% 0 0); + --input: oklch(88% 0 0); + --ring: oklch(25% 0 0); + + --sidebar-background: oklch(98% 0 0); + --sidebar-foreground: oklch(35% 0 0); + --sidebar-primary: oklch(25% 0 0); + --sidebar-primary-foreground: oklch(100% 0 0); + --sidebar-accent: oklch(94% 0 0); + --sidebar-accent-foreground: oklch(20% 0 0); + --sidebar-border: oklch(88% 0 0); + --sidebar-ring: oklch(25% 0 0); +} + +/* Minimal Theme - Dark (Space Grotesk) */ +[data-theme='minimal-dark'] { + --font-family: var(--font-space-grotesk); + + --background: oklch(10% 0 0); + --foreground: oklch(98% 0 0); + + --card: oklch(13% 0 0); + --card-foreground: oklch(98% 0 0); + + --popover: oklch(13% 0 0); + --popover-foreground: oklch(98% 0 0); + + --primary: oklch(98% 0 0); + --primary-foreground: oklch(10% 0 0); + + --secondary: oklch(20% 0 0); + --secondary-foreground: oklch(98% 0 0); + + --muted: oklch(20% 0 0); + --muted-foreground: oklch(65% 0 0); + + --accent: oklch(20% 0 0); + --accent-foreground: oklch(98% 0 0); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0 0); + + --border: oklch(25% 0 0); + --input: oklch(25% 0 0); + --ring: oklch(85% 0 0); + + --sidebar-background: oklch(8% 0 0); + --sidebar-foreground: oklch(70% 0 0); + --sidebar-primary: oklch(98% 0 0); + --sidebar-primary-foreground: oklch(10% 0 0); + --sidebar-accent: oklch(18% 0 0); + --sidebar-accent-foreground: oklch(98% 0 0); + --sidebar-border: oklch(22% 0 0); + --sidebar-ring: oklch(85% 0 0); +} + +/* Apply font family globally when theme is set */ +[data-theme] body, +:root body { + font-family: var(--font-family, var(--font-inter)); +} diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index f48955c..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,82 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - darkMode: ['class'], - content: ['./src/**/*.{ts,tsx,html}'], - prefix: '', - theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px' - } - }, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - keyframes: { - 'accordion-down': { - from: { height: '0' }, - to: { height: 'var(--radix-accordion-content-height)' } - }, - 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: '0' } - }, - shine: { - from: { backgroundPosition: '200% 0' }, - to: { backgroundPosition: '-200% 0' } - }, - 'fade-in': { - from: { opacity: '0' }, - to: { opacity: '1' } - } - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - shine: 'shine 8s ease-in-out infinite', - 'fade-in': 'fade-in 0.2s ease-out' - } - } - }, - plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')] -} diff --git a/tsconfig.node.json b/tsconfig.node.json index c595c08..b760e00 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -8,10 +8,11 @@ ], "compilerOptions": { "composite": true, + "moduleResolution": "bundler", "types": [ "electron-vite/node" ], "noUnusedLocals": false, - "noUnusedParameters": false, + "noUnusedParameters": false } } From e0515d5f533b2f3a5a81620d913e34c7eaa24a2b Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Sun, 18 Jan 2026 21:24:13 +0530 Subject: [PATCH 02/26] chore: upgrade ESLint to v9 with flat config - Upgrade ESLint from v8 to v9.39 - Convert .eslintrc.cjs to eslint.config.mjs (flat config format) - Remove deprecated .eslintignore file - Update lint script in package.json --- .eslintignore | 4 - .eslintrc.cjs | 30 -- eslint.config.mjs | 59 +++ package.json | 6 +- pnpm-lock.yaml | 377 ++++++++---------- .../src/components/patient/NewPatientForm.tsx | 2 +- 6 files changed, 238 insertions(+), 240 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.cjs create mode 100644 eslint.config.mjs diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index a6f34fe..0000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -out -.gitignore diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index ae8f0f8..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - settings: { - react: { - version: 'detect' - } - }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@tanstack/eslint-plugin-query/recommended', - 'plugin:react/jsx-runtime', - '@electron-toolkit/eslint-config-ts/recommended', - '@electron-toolkit/eslint-config-prettier' - ], - rules: { - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_' - } - ], - '@typescript-eslint/explicit-function-return-type': 'off', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn' - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..333a3c0 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,59 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' +import reactPlugin from 'eslint-plugin-react' +import reactHooksPlugin from 'eslint-plugin-react-hooks' +import tanstackQuery from '@tanstack/eslint-plugin-query' + +export default tseslint.config( + { + ignores: ['dist', 'out', 'node_modules', '*.config.js', '*.config.mjs', '*.config.ts'] + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + plugins: { + react: reactPlugin, + 'react-hooks': reactHooksPlugin, + '@tanstack/query': tanstackQuery + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + }, + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, + settings: { + react: { + version: 'detect' + } + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactPlugin.configs['jsx-runtime'].rules, + ...reactHooksPlugin.configs.recommended.rules, + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + } + ], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn' + } + } +) diff --git a/package.json b/package.json index 2737480..8b0c7b5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "scripts": { "format": "prettier --write .", - "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", + "lint": "eslint . --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "pnpm typecheck:node && pnpm typecheck:web", @@ -88,6 +88,7 @@ "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^1.0.1", "@electron/rebuild": "^3.7.1", + "@eslint/js": "^9.39.2", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.0.0", "@tanstack/eslint-plugin-query": "^5.62.8", @@ -99,9 +100,10 @@ "electron": "^33.3.1", "electron-builder": "^25.1.8", "electron-vite": "^2.3.0", - "eslint": "^8.57.1", + "eslint": "^9.39.2", "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", + "globals": "^17.0.0", "husky": "^9.1.7", "postcss": "^8.4.49", "prettier": "^3.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14c6ff3..06cbff0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,16 +152,19 @@ importers: version: 19.8.1 '@electron-toolkit/eslint-config-prettier': specifier: ^2.0.0 - version: 2.0.0(eslint@8.57.1)(prettier@3.8.0) + version: 2.0.0(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) '@electron-toolkit/eslint-config-ts': specifier: ^3.1.0 - version: 3.1.0(eslint@8.57.1)(typescript@5.9.3) + version: 3.1.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@electron-toolkit/tsconfig': specifier: ^1.0.1 version: 1.0.1(@types/node@22.19.7) '@electron/rebuild': specifier: ^3.7.1 version: 3.7.2 + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.19(tailwindcss@4.1.18) @@ -170,7 +173,7 @@ importers: version: 4.1.18(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)) '@tanstack/eslint-plugin-query': specifier: ^5.62.8 - version: 5.91.3(eslint@8.57.1)(typescript@5.9.3) + version: 5.91.3(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.13 @@ -196,14 +199,17 @@ importers: specifier: ^2.3.0 version: 2.3.0(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)) eslint: - specifier: ^8.57.1 - version: 8.57.1 + specifier: ^9.39.2 + version: 9.39.2(jiti@2.6.1) eslint-plugin-react: specifier: ^7.37.3 - version: 7.37.5(eslint@8.57.1) + version: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: ^5.1.0 - version: 5.2.0(eslint@8.57.1) + version: 5.2.0(eslint@9.39.2(jiti@2.6.1)) + globals: + specifier: ^17.0.0 + version: 17.0.0 husky: specifier: ^9.1.7 version: 9.1.7 @@ -771,18 +777,34 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.2': resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -806,18 +828,21 @@ packages: peerDependencies: react-hook-form: ^7.0.0 - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} @@ -862,18 +887,6 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@npmcli/fs@2.1.2': resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -1821,6 +1834,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -1927,9 +1943,6 @@ packages: resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2457,10 +2470,6 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -2637,9 +2646,9 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} @@ -2649,15 +2658,19 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} @@ -2706,9 +2719,6 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -2721,9 +2731,9 @@ packages: picomatch: optional: true - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -2739,9 +2749,9 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -2869,14 +2879,18 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + globals@17.0.0: + resolution: {integrity: sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -2897,9 +2911,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -3112,10 +3123,6 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3845,9 +3852,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -3998,10 +4002,6 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -4019,9 +4019,6 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -4267,9 +4264,6 @@ packages: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4317,10 +4311,6 @@ packages: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -4794,21 +4784,21 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - '@electron-toolkit/eslint-config-prettier@2.0.0(eslint@8.57.1)(prettier@3.8.0)': + '@electron-toolkit/eslint-config-prettier@2.0.0(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0)': dependencies: - eslint: 8.57.1 - eslint-config-prettier: 9.1.2(eslint@8.57.1) - eslint-plugin-prettier: 5.5.5(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.0) + eslint: 9.39.2(jiti@2.6.1) + eslint-config-prettier: 9.1.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-prettier: 5.5.5(eslint-config-prettier@9.1.2(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) prettier: 3.8.0 transitivePeerDependencies: - '@types/eslint' - '@electron-toolkit/eslint-config-ts@3.1.0(eslint@8.57.1)(typescript@5.9.3)': + '@electron-toolkit/eslint-config-ts@3.1.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint/js': 9.39.2 - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) globals: 16.5.0 - typescript-eslint: 8.53.0(eslint@8.57.1)(typescript@5.9.3) + typescript-eslint: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -5080,19 +5070,35 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/eslintrc@2.1.4': + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 + espree: 10.4.0 + globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 @@ -5101,10 +5107,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} - '@eslint/js@9.39.2': {} + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -5128,17 +5139,16 @@ snapshots: dependencies: react-hook-form: 7.71.1(react@18.3.1) - '@humanwhocodes/config-array@0.13.0': + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} '@isaacs/balanced-match@4.0.1': {} @@ -5192,18 +5202,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - '@npmcli/fs@2.1.2': dependencies: '@gar/promisify': 1.1.3 @@ -5849,10 +5847,10 @@ snapshots: tailwindcss: 4.1.18 vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2) - '@tanstack/eslint-plugin-query@5.91.3(eslint@8.57.1)(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.91.3(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) - eslint: 8.57.1 + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -6102,6 +6100,8 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} + '@types/json-schema@7.0.15': {} + '@types/keyv@3.1.4': dependencies: '@types/node': 22.19.7 @@ -6156,15 +6156,15 @@ snapshots: '@types/node': 22.19.7 optional: true - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/type-utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.53.0 - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -6172,14 +6172,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6202,13 +6202,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.53.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.53.0 '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -6231,13 +6231,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.53.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.53.0 '@typescript-eslint/types': 8.53.0 '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6247,8 +6247,6 @@ snapshots: '@typescript-eslint/types': 8.53.0 eslint-visitor-keys: 4.2.1 - '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@babel/core': 7.28.6 @@ -6903,10 +6901,6 @@ snapshots: dependencies: esutils: 2.0.3 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -7197,24 +7191,24 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@9.1.2(eslint@8.57.1): + eslint-config-prettier@9.1.2(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-prettier@5.5.5(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.0): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@9.1.2(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0): dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) prettier: 3.8.0 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: - eslint-config-prettier: 9.1.2(eslint@8.57.1) + eslint-config-prettier: 9.1.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-react@7.37.5(eslint@8.57.1): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -7222,7 +7216,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -7236,7 +7230,7 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-scope@7.2.2: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -7245,54 +7239,52 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@8.57.1: + eslint@9.39.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 - doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color - espree@9.6.1: + espree@10.4.0: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 + eslint-visitor-keys: 4.2.1 esquery@1.7.0: dependencies: @@ -7333,10 +7325,6 @@ snapshots: fast-uri@3.1.0: {} - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -7345,9 +7333,9 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 file-uri-to-path@1.0.0: {} @@ -7366,11 +7354,10 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: flatted: 3.3.3 keyv: 4.5.4 - rimraf: 3.0.2 flatted@3.3.3: {} @@ -7539,12 +7526,12 @@ snapshots: dependencies: ini: 4.1.1 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} globals@16.5.0: {} + globals@17.0.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -7572,8 +7559,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -7782,8 +7767,6 @@ snapshots: is-obj@2.0.0: {} - is-path-inside@3.0.3: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -8523,8 +8506,6 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - quick-lru@5.1.1: {} rc@1.2.8: @@ -8684,8 +8665,6 @@ snapshots: retry@0.12.0: {} - reusify@1.1.0: {} - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -8733,10 +8712,6 @@ snapshots: rope-sequence@1.3.4: {} - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -9036,8 +9011,6 @@ snapshots: text-extensions@2.4.0: {} - text-table@0.2.0: {} - through@2.3.8: {} tiny-typed-emitter@2.1.0: {} @@ -9080,8 +9053,6 @@ snapshots: type-fest@0.13.1: optional: true - type-fest@0.20.2: {} - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -9115,13 +9086,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.53.0(eslint@8.57.1)(typescript@5.9.3): + typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.53.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@8.57.1)(typescript@5.9.3) - eslint: 8.57.1 + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/src/renderer/src/components/patient/NewPatientForm.tsx b/src/renderer/src/components/patient/NewPatientForm.tsx index 9ac7514..4745695 100644 --- a/src/renderer/src/components/patient/NewPatientForm.tsx +++ b/src/renderer/src/components/patient/NewPatientForm.tsx @@ -132,7 +132,7 @@ export const NewPatientForm = forwardRef Date: Sun, 18 Jan 2026 22:12:58 +0530 Subject: [PATCH 03/26] feat: redesign dashboard with activity log and modern UI - Add activity_log table to track all entity changes (patients, surgeries, followups) - Log activities on create/update for patients, surgeries, and followups - Redesign dashboard with modern bento-style layout - Add time-based greeting header with date display - Create professional stats cards with monthly change indicators - Add prominent Add Patient hero card with decorative elements - Redesign nav cards with centered vertical layout - Update Recent Activity to use activity log with relative timestamps - Add cursor-pointer globally for all clickable elements - Add staggered fade-in animation for activity items --- src/main/api.ts | 5 +- src/main/db/migrations.ts | 4 +- src/main/db/migrations/007_activity_log.ts | 34 ++ src/main/repository/activity.ts | 103 +++++ src/main/repository/dashboard.ts | 59 ++- src/main/repository/patient.ts | 25 +- src/main/repository/surgery.ts | 96 +++- src/renderer/src/index.css | 33 ++ src/renderer/src/lib/queries.ts | 2 +- src/renderer/src/routes/home.tsx | 511 ++++++++++++--------- src/shared/types/db.d.ts | 16 + 11 files changed, 663 insertions(+), 225 deletions(-) create mode 100644 src/main/db/migrations/007_activity_log.ts create mode 100644 src/main/repository/activity.ts diff --git a/src/main/api.ts b/src/main/api.ts index efc5aab..2ac80ac 100644 --- a/src/main/api.ts +++ b/src/main/api.ts @@ -45,7 +45,8 @@ import { getTemplateTags } from './repository/surgery-template' import { createBackup, listBackups, deleteBackup, restoreBackup } from './backup' -import { getDashboardStats, getRecentActivity } from './repository/dashboard' +import { getDashboardStats } from './repository/dashboard' +import { getRecentActivities } from './repository/activity' // Custom APIs for renderer export const api = { @@ -95,7 +96,7 @@ export const api = { restoreBackup, getDashboardStats, - getRecentActivity + getRecentActivities } export type ApiType = typeof api diff --git a/src/main/db/migrations.ts b/src/main/db/migrations.ts index d1e8fc1..1ced77b 100644 --- a/src/main/db/migrations.ts +++ b/src/main/db/migrations.ts @@ -5,6 +5,7 @@ import * as m_003_doa_dod from './migrations/003_doa_dod' import * as m_004_surgery_templates from './migrations/004_surgery_templates' import * as m_005_backup_settings from './migrations/005_backup_settings' import * as m_006_onboarding_setting from './migrations/006_onboarding_setting' +import * as m_007_activity_log from './migrations/007_activity_log' export default { '000_init': m_000_init, @@ -13,5 +14,6 @@ export default { '003_doa_dod': m_003_doa_dod, '004_surgery_templates': m_004_surgery_templates, '005_backup_settings': m_005_backup_settings, - '006_onboarding_setting': m_006_onboarding_setting + '006_onboarding_setting': m_006_onboarding_setting, + '007_activity_log': m_007_activity_log } diff --git a/src/main/db/migrations/007_activity_log.ts b/src/main/db/migrations/007_activity_log.ts new file mode 100644 index 0000000..68a0858 --- /dev/null +++ b/src/main/db/migrations/007_activity_log.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('activity_log') + .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey()) + .addColumn('entity_type', 'text', (col) => col.notNull()) // 'patient', 'surgery', 'followup', 'doctor' + .addColumn('entity_id', 'integer', (col) => col.notNull()) + .addColumn('action', 'text', (col) => col.notNull()) // 'created', 'updated', 'deleted' + .addColumn('title', 'text', (col) => col.notNull()) // Display title for the activity + .addColumn('description', 'text') // Optional description + .addColumn('patient_id', 'integer') // For navigation - the patient this relates to + .addColumn('surgery_id', 'integer') // For navigation - the surgery this relates to (if applicable) + .addColumn('created_at', 'integer', (col) => col.notNull()) + .execute() + + // Index for efficient querying + await db.schema + .createIndex('activity_log_created_at_idx') + .on('activity_log') + .column('created_at') + .execute() + + await db.schema + .createIndex('activity_log_entity_idx') + .on('activity_log') + .columns(['entity_type', 'entity_id']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('activity_log').execute() +} diff --git a/src/main/repository/activity.ts b/src/main/repository/activity.ts new file mode 100644 index 0000000..1ce1050 --- /dev/null +++ b/src/main/repository/activity.ts @@ -0,0 +1,103 @@ +import { db } from '../db' + +export type EntityType = 'patient' | 'surgery' | 'followup' | 'doctor' +export type ActionType = 'created' | 'updated' | 'deleted' + +export interface ActivityLogEntry { + entityType: EntityType + entityId: number + action: ActionType + title: string + description?: string + patientId?: number + surgeryId?: number +} + +export interface ActivityLogItem { + id: number + entityType: EntityType + entityId: number + action: ActionType + title: string + description: string | null + patientId: number | null + surgeryId: number | null + createdAt: string +} + +/** + * Log an activity to the activity log + */ +export async function logActivity(entry: ActivityLogEntry): Promise { + await db + .insertInto('activity_log') + .values({ + entity_type: entry.entityType, + entity_id: entry.entityId, + action: entry.action, + title: entry.title, + description: entry.description ?? null, + patient_id: entry.patientId ?? null, + surgery_id: entry.surgeryId ?? null, + created_at: Date.now() + }) + .execute() +} + +/** + * Get recent activities with pagination + */ +export async function getRecentActivities(limit: number = 10): Promise { + const results = await db + .selectFrom('activity_log') + .selectAll() + .orderBy('created_at', 'desc') + .limit(limit) + .execute() + + return results.map((row) => ({ + id: row.id, + entityType: row.entity_type as EntityType, + entityId: row.entity_id, + action: row.action as ActionType, + title: row.title, + description: row.description, + patientId: row.patient_id, + surgeryId: row.surgery_id, + createdAt: row.created_at instanceof Date + ? row.created_at.toISOString() + : new Date(row.created_at as number).toISOString() + })) +} + +/** + * Get activities for a specific entity + */ +export async function getActivitiesForEntity( + entityType: EntityType, + entityId: number, + limit: number = 20 +): Promise { + const results = await db + .selectFrom('activity_log') + .selectAll() + .where('entity_type', '=', entityType) + .where('entity_id', '=', entityId) + .orderBy('created_at', 'desc') + .limit(limit) + .execute() + + return results.map((row) => ({ + id: row.id, + entityType: row.entity_type as EntityType, + entityId: row.entity_id, + action: row.action as ActionType, + title: row.title, + description: row.description, + patientId: row.patient_id, + surgeryId: row.surgery_id, + createdAt: row.created_at instanceof Date + ? row.created_at.toISOString() + : new Date(row.created_at as number).toISOString() + })) +} diff --git a/src/main/repository/dashboard.ts b/src/main/repository/dashboard.ts index 9b02a4f..e06f7fc 100644 --- a/src/main/repository/dashboard.ts +++ b/src/main/repository/dashboard.ts @@ -11,10 +11,13 @@ export interface DashboardStats { export interface RecentActivityItem { id: number - type: 'patient' | 'surgery' + type: 'patient' | 'surgery' | 'followup' title: string subtitle: string date: string + // Navigation fields + patientId?: number + surgeryId?: number } export async function getDashboardStats(): Promise { @@ -78,16 +81,48 @@ export async function getRecentActivity(limit: number = 10): Promise { + // Get recent followups with surgery and patient info + const recentFollowups = await db + .selectFrom('surgery_followups') + .innerJoin('surgeries', 'surgeries.id', 'surgery_followups.surgery_id') + .innerJoin('patients', 'patients.id', 'surgeries.patient_id') + .select([ + 'surgery_followups.id', + 'surgery_followups.notes', + 'surgery_followups.created_at', + 'surgery_followups.surgery_id', + 'surgeries.title as surgery_title', + 'surgeries.patient_id', + 'patients.name as patient_name' + ]) + .orderBy('surgery_followups.created_at', 'desc') + .limit(limit) + .execute() + + // Helper to convert Date or timestamp to ISO string + const dateToString = (date: Date | number | null | undefined): string => { if (!date) return '' - return date instanceof Date ? date.toISOString() : String(date) + if (date instanceof Date) { + return date.toISOString() + } + // Handle numeric timestamps + if (typeof date === 'number') { + return new Date(date).toISOString() + } + return '' + } + + // Helper to extract preview from notes (strip HTML and truncate) + const getNotesPreview = (notes: string): string => { + const text = notes.replace(/<[^>]*>/g, '').trim() + return text.length > 50 ? text.slice(0, 50) + '...' : text } // Combine and sort by date @@ -97,14 +132,26 @@ export async function getRecentActivity(limit: number = 10): Promise ({ id: s.id, type: 'surgery' as const, title: s.name, subtitle: s.title || `BHT: ${s.bht}`, - date: dateToString(s.created_at) + date: dateToString(s.created_at), + patientId: s.patient_id, + surgeryId: s.id + })), + ...recentFollowups.map((f) => ({ + id: f.id, + type: 'followup' as const, + title: f.patient_name, + subtitle: f.surgery_title ? `${f.surgery_title}: ${getNotesPreview(f.notes)}` : getNotesPreview(f.notes), + date: dateToString(f.created_at), + patientId: f.patient_id, + surgeryId: f.surgery_id })) ] diff --git a/src/main/repository/patient.ts b/src/main/repository/patient.ts index 5ede853..9c6f0c5 100644 --- a/src/main/repository/patient.ts +++ b/src/main/repository/patient.ts @@ -4,10 +4,24 @@ import { db } from '../db' import { NewWithoutTimestamps, addTimestamps } from '../utils/sql' import { PatientModel } from '../../shared/models/PatientModel' import { PatientFilter } from '../../shared/types/api' +import { logActivity } from './activity' export const createNewPatient = async (patient: NewWithoutTimestamps) => { const data = addTimestamps(patient) - return await db.insertInto('patients').values(data).returningAll().executeTakeFirst() + const result = await db.insertInto('patients').values(data).returningAll().executeTakeFirst() + + if (result) { + await logActivity({ + entityType: 'patient', + entityId: result.id, + action: 'created', + title: result.name, + description: `PHN: ${result.phn}`, + patientId: result.id + }) + } + + return result } export const getPatientById = async (id: number) => { @@ -45,6 +59,15 @@ export const updatePatientById = async (id: number, patient: PatientUpdate) => { .executeTakeFirst() if (result) { + await logActivity({ + entityType: 'patient', + entityId: result.id, + action: 'updated', + title: result.name, + description: `PHN: ${result.phn}`, + patientId: result.id + }) + return new PatientModel(result) } diff --git a/src/main/repository/surgery.ts b/src/main/repository/surgery.ts index 1d5d65d..cb5d8e6 100644 --- a/src/main/repository/surgery.ts +++ b/src/main/repository/surgery.ts @@ -6,10 +6,32 @@ import { SurgeryFilter } from '../../shared/types/api' import { SurgeryModel } from '../../shared/models/SurgeryModel' import { DoctorModel } from '../../shared/models/DoctorModel' import { FollowupModel } from '../../shared/models/FollowupModel' +import { logActivity } from './activity' export const createNewSurgery = async (surgery: NewWithoutTimestamps) => { const data = addTimestamps(surgery) - return await db.insertInto('surgeries').values(data).returningAll().executeTakeFirst() + const result = await db.insertInto('surgeries').values(data).returningAll().executeTakeFirst() + + if (result) { + // Get patient name for the activity log + const patient = await db + .selectFrom('patients') + .where('id', '=', result.patient_id) + .select('name') + .executeTakeFirst() + + await logActivity({ + entityType: 'surgery', + entityId: result.id, + action: 'created', + title: result.title || `BHT: ${result.bht}`, + description: patient?.name ? `Patient: ${patient.name}` : undefined, + patientId: result.patient_id, + surgeryId: result.id + }) + } + + return result } export const updateSurgery = async ( @@ -21,12 +43,33 @@ export const updateSurgery = async ( updatedAt: true }) - return await db + const result = await db .updateTable('surgeries') .set(data) .where('id', '=', id) .returningAll() .executeTakeFirst() + + if (result) { + // Get patient name for the activity log + const patient = await db + .selectFrom('patients') + .where('id', '=', result.patient_id) + .select('name') + .executeTakeFirst() + + await logActivity({ + entityType: 'surgery', + entityId: result.id, + action: 'updated', + title: result.title || `BHT: ${result.bht}`, + description: patient?.name ? `Patient: ${patient.name}` : undefined, + patientId: result.patient_id, + surgeryId: result.id + }) + } + + return result } export const updateSurgeryDoctorsDoneBy = async (surgeryId: number, doctorIds: number[]) => { @@ -140,6 +183,31 @@ export const createNewFollowUp = async (surgeryId: number, notes: string) => { return null } + // Get surgery and patient info for activity log + const surgeryInfo = await db + .selectFrom('surgeries') + .innerJoin('patients', 'patients.id', 'surgeries.patient_id') + .where('surgeries.id', '=', surgeryId) + .select(['surgeries.title', 'surgeries.patient_id', 'patients.name as patient_name']) + .executeTakeFirst() + + if (surgeryInfo) { + // Extract preview from notes (strip HTML and truncate) + const notesPreview = notes.replace(/<[^>]*>/g, '').trim().slice(0, 50) + + await logActivity({ + entityType: 'followup', + entityId: result.id, + action: 'created', + title: surgeryInfo.patient_name, + description: surgeryInfo.title + ? `${surgeryInfo.title}: ${notesPreview}${notesPreview.length >= 50 ? '...' : ''}` + : notesPreview, + patientId: surgeryInfo.patient_id, + surgeryId: surgeryId + }) + } + return new FollowupModel(result) } @@ -163,6 +231,30 @@ export const updateFollowUp = async (id: number, notes: string) => { return null } + // Get surgery and patient info for activity log + const surgeryInfo = await db + .selectFrom('surgeries') + .innerJoin('patients', 'patients.id', 'surgeries.patient_id') + .where('surgeries.id', '=', result.surgery_id) + .select(['surgeries.title', 'surgeries.patient_id', 'patients.name as patient_name']) + .executeTakeFirst() + + if (surgeryInfo) { + const notesPreview = notes.replace(/<[^>]*>/g, '').trim().slice(0, 50) + + await logActivity({ + entityType: 'followup', + entityId: result.id, + action: 'updated', + title: surgeryInfo.patient_name, + description: surgeryInfo.title + ? `${surgeryInfo.title}: ${notesPreview}${notesPreview.length >= 50 ? '...' : ''}` + : notesPreview, + patientId: surgeryInfo.patient_id, + surgeryId: result.surgery_id + }) + } + return new FollowupModel(result) } diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index 544ecd4..f9aae71 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -118,6 +118,19 @@ transform: scale(1); } } + + @keyframes stagger-fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + --animate-stagger-fade-in: stagger-fade-in 0.3s ease-out both; } /* Base semantic colors using CSS custom properties */ @@ -442,6 +455,10 @@ animation-duration: 300ms; } +@utility animate-stagger-fade-in { + animation: stagger-fade-in 0.3s ease-out both; +} + @keyframes enter { from { opacity: var(--tw-enter-opacity, 1); @@ -553,6 +570,22 @@ font-family: var(--font-sans); } + /* Hand cursor for all clickable elements */ + button, + [role="button"], + [type="button"], + [type="submit"], + [type="reset"], + a, + select, + summary, + [tabindex]:not([tabindex="-1"]), + input[type="checkbox"], + input[type="radio"], + label[for] { + cursor: pointer; + } + /* Scrollbar */ ::-webkit-scrollbar { width: 0.5rem; diff --git a/src/renderer/src/lib/queries.ts b/src/renderer/src/lib/queries.ts index 6d8a1cd..bc5880a 100644 --- a/src/renderer/src/lib/queries.ts +++ b/src/renderer/src/lib/queries.ts @@ -92,7 +92,7 @@ export const queries = createQueryKeyStore({ }, recentActivity: (limit: number = 10) => ({ queryKey: [limit], - queryFn: () => unwrapResult(window.api.invoke('getRecentActivity', limit)) + queryFn: () => unwrapResult(window.api.invoke('getRecentActivities', limit)) }) } }) diff --git a/src/renderer/src/routes/home.tsx b/src/renderer/src/routes/home.tsx index dd885a8..62f88c4 100644 --- a/src/renderer/src/routes/home.tsx +++ b/src/renderer/src/routes/home.tsx @@ -1,278 +1,365 @@ import { useQuery } from '@tanstack/react-query' import { queries } from '@renderer/lib/queries' import { useNavigate } from 'react-router-dom' -import { useSettings } from '@renderer/contexts/SettingsContext' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@renderer/components/ui/card' -import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { Skeleton } from '@renderer/components/ui/skeleton' import { Users, Stethoscope, UserCog, Plus, - TrendingUp, - Calendar, ArrowRight, - Activity + Activity, + Sun, + Moon, + CloudSun, + ClipboardList } from 'lucide-react' -import { formatDate } from '@renderer/lib/utils' +import { format } from 'date-fns' +import { cn } from '@renderer/lib/utils' -interface StatCardProps { - title: string - value: number | string - description?: string - icon: React.ReactNode - trend?: string - isLoading?: boolean +// Helper to format relative time (e.g., "2h ago") +function formatRelativeTime(dateStr: string): string { + if (!dateStr) return '' + + const date = new Date(dateStr) + if (isNaN(date.getTime())) return '' + + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffSec = Math.floor(diffMs / 1000) + const diffMin = Math.floor(diffSec / 60) + const diffHr = Math.floor(diffMin / 60) + const diffDays = Math.floor(diffHr / 24) + + if (diffSec < 60) return 'just now' + if (diffMin < 60) return `${diffMin}m ago` + if (diffHr < 24) return `${diffHr}h ago` + if (diffDays < 7) return `${diffDays}d ago` + return format(date, 'MMM d') +} + +// Greeting header with time-based icon +function GreetingHeader() { + const hour = new Date().getHours() + const greeting = hour < 12 ? 'Good Morning' : hour < 17 ? 'Good Afternoon' : 'Good Evening' + const Icon = hour < 12 ? Sun : hour < 17 ? CloudSun : Moon + const iconColor = hour < 12 ? 'text-amber-500' : hour < 17 ? 'text-orange-500' : 'text-indigo-400' + + return ( +
    +
    + + {greeting} +
    + {format(new Date(), 'EEEE, MMMM d')} +
    + ) } -function StatCard({ title, value, description, icon, trend, isLoading }: StatCardProps) { +// Stats cards - individual cards for each metric +interface StatsCardsProps { + stats: { + totalPatients: number + totalSurgeries: number + totalDoctors: number + patientsThisMonth: number + surgeriesThisMonth: number + } | null + isLoading: boolean +} + +function StatsCards({ stats, isLoading }: StatsCardsProps) { + const items = [ + { + icon: Users, + value: stats?.totalPatients ?? 0, + label: 'Total Patients', + change: stats?.patientsThisMonth || 0, + changeLabel: 'this month' + }, + { + icon: Stethoscope, + value: stats?.totalSurgeries ?? 0, + label: 'Surgeries', + change: stats?.surgeriesThisMonth || 0, + changeLabel: 'this month' + }, + { + icon: UserCog, + value: stats?.totalDoctors ?? 0, + label: 'Doctors', + change: null, + changeLabel: 'registered' + } + ] + return ( - - - {title} -
    - {icon} +
    + {items.map((item, i) => ( + + +
    +
    +

    {item.label}

    + {isLoading ? ( + + ) : ( +

    {item.value}

    + )} + {isLoading ? ( + + ) : ( +

    + {item.change !== null ? ( + +{item.change} + ) : null} + {item.change !== null ? ' ' : ''} + {item.changeLabel} +

    + )} +
    +
    + +
    +
    +
    + {/* Decorative gradient accent */} +
    + + ))} +
    + ) +} + +// Hero CTA - Add Patient card +interface AddPatientCardProps { + className?: string +} + +function AddPatientCard({ className }: AddPatientCardProps) { + const navigate = useNavigate() + + return ( + ) } -interface QuickActionProps { +// Navigation card +interface NavCardProps { + to: string + icon: React.ComponentType<{ className?: string }> title: string description: string - icon: React.ReactNode - onClick: () => void + className?: string } -function QuickAction({ title, description, icon, onClick }: QuickActionProps) { +function NavCard({ to, icon: Icon, title, description, className }: NavCardProps) { + const navigate = useNavigate() + return ( ) } +// Recent activity card with vertical scrollable list interface RecentActivityItem { id: number - type: 'patient' | 'surgery' + entityType: 'patient' | 'surgery' | 'followup' | 'doctor' + entityId: number + action: 'created' | 'updated' | 'deleted' title: string - subtitle: string - date: string + description: string | null + patientId: number | null + surgeryId: number | null + createdAt: string } -function RecentActivityList({ - items, - isLoading -}: { +interface RecentActivityCardProps { items: RecentActivityItem[] isLoading: boolean -}) { + className?: string +} + +function RecentActivityCard({ items, isLoading, className }: RecentActivityCardProps) { const navigate = useNavigate() - if (isLoading) { - return ( -
    - {[1, 2, 3, 4, 5].map((i) => ( -
    - -
    - - -
    - -
    - ))} -
    - ) + const getTypeStyles = (entityType: RecentActivityItem['entityType'], action: RecentActivityItem['action']) => { + const actionLabel = action === 'created' ? 'added' : action === 'updated' ? 'updated' : 'deleted' + + switch (entityType) { + case 'patient': + return { + dot: 'bg-blue-500', + icon: Users, + label: `Patient ${actionLabel}` + } + case 'surgery': + return { + dot: 'bg-green-500', + icon: Stethoscope, + label: `Surgery ${actionLabel}` + } + case 'followup': + return { + dot: 'bg-amber-500', + icon: ClipboardList, + label: `Follow-up ${actionLabel}` + } + case 'doctor': + return { + dot: 'bg-purple-500', + icon: UserCog, + label: `Doctor ${actionLabel}` + } + } } - if (items.length === 0) { - return ( -
    - -

    No recent activity

    -
    - ) + const handleClick = (item: RecentActivityItem) => { + if (item.surgeryId && item.patientId) { + navigate(`/patients/${item.patientId}/surgeries/${item.surgeryId}`) + } else if (item.patientId) { + navigate(`/patients/${item.patientId}`) + } else if (item.entityType === 'doctor') { + navigate(`/doctors/${item.entityId}`) + } } return ( -
    - {items.map((item) => ( - - ))} -
    + ) : ( +
    + {items.map((item, index) => { + const styles = getTypeStyles(item.entityType, item.action) + return ( + + ) + })} +
    + )} + +
    ) } export default function Home() { - const navigate = useNavigate() - const { settings } = useSettings() - const { data: stats, isLoading: statsLoading } = useQuery({ ...queries.dashboard.stats, - refetchInterval: 30000 // Refresh every 30 seconds + refetchInterval: 30000 }) const { data: recentActivity, isLoading: activityLoading } = useQuery({ - ...queries.dashboard.recentActivity(10) + ...queries.dashboard.recentActivity(15) }) return ( -
    - {/* Welcome header */} -
    -

    - Welcome to Op Notes -

    - {settings?.hospital && ( -

    - {settings.hospital} - {settings.unit && ` - ${settings.unit}`} -

    - )} -
    +
    + {/* Row 1: Greeting Header */} + - {/* Stats cards */} -
    - } - trend={stats?.patientsThisMonth ? '+' : undefined} - isLoading={statsLoading} - /> - } - trend={stats?.surgeriesThisMonth ? '+' : undefined} - isLoading={statsLoading} - /> - } - isLoading={statsLoading} - /> - } - isLoading={statsLoading} - /> -
    + {/* Row 2: Stats Cards */} + -
    - {/* Quick Actions */} - - - Quick Actions - Common tasks you can perform - - - } - onClick={() => navigate('/patients/add')} - /> - } - onClick={() => navigate('/patients')} - /> - } - onClick={() => navigate('/surgeries')} - /> - } - onClick={() => navigate('/doctors')} - /> - - + {/* Row 3: Main Content Grid */} +
    + {/* Left Column: Add Patient Hero + Nav Cards */} +
    + {/* Hero CTA */} + - {/* Recent Activity */} - - - Recent Activity - Latest patients and surgeries - - - - - + {/* Quick Links - 2x2 grid */} +
    + + + + +
    +
    + + {/* Right Column: Recent Activity */} +
    ) diff --git a/src/shared/types/db.d.ts b/src/shared/types/db.d.ts index 495a57e..44b7c58 100644 --- a/src/shared/types/db.d.ts +++ b/src/shared/types/db.d.ts @@ -107,6 +107,18 @@ export interface SurgeryTemplatesFTS { content: string } +export interface ActivityLogTable { + id: Generated + entity_type: 'patient' | 'surgery' | 'followup' | 'doctor' + entity_id: number + action: 'created' | 'updated' | 'deleted' + title: string + description: string | null + patient_id: number | null + surgery_id: number | null + created_at: ColumnType +} + export interface Database { patients: PatientTable surgeries: SurgeryTable @@ -115,6 +127,7 @@ export interface Database { surgery_doctors_assisted_by: SurgeryDoctorsAssistedByTable surgery_followups: SurgeryFollowUpTable surgery_templates: SurgeryTemplateTable + activity_log: ActivityLogTable patients_fts: PatientsFTS surgeries_fts: SurgeriesFTS @@ -147,3 +160,6 @@ export type AppSettingUpdate = Updateable export type SurgeryTemplate = Selectable export type NewSurgeryTemplate = Insertable export type SurgeryTemplateUpdate = Updateable + +export type ActivityLog = Selectable +export type NewActivityLog = Insertable From 0a13f911d6133d0c7b6e1db07505cab203d467e5 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Sun, 18 Jan 2026 23:19:01 +0530 Subject: [PATCH 04/26] feat: overhaul theme system with 9 themes and bundled fonts - Add 9 distinct themes: Aurora, Meadow, Bloom, Latte, Slate, Midnight, Obsidian, Ocean, and Retro (each with light/dark variants) - Bundle fonts via @fontsource packages for offline use: Inter, Source Sans 3, Nunito, VT323, JetBrains Mono - Create ThemeDropdown component with visual 3x3 theme grid - Create ModeToggle component with animated sun/moon icons - Update ThemeSwitcher with theme preview cards - Set Ocean Dark as default theme - Enable GPU-accelerated font rendering with antialiasing --- package.json | 5 + pnpm-lock.yaml | 40 + src/renderer/src/components/layout/Header.tsx | 32 +- .../src/components/layout/Sidebar.tsx | 34 +- .../src/components/theme/ModeToggle.tsx | 116 ++ .../src/components/theme/ThemeDropdown.tsx | 283 ++++ .../src/components/theme/ThemeSwitcher.tsx | 229 ++- src/renderer/src/components/ui/button.tsx | 6 +- src/renderer/src/components/ui/card.tsx | 14 +- src/renderer/src/contexts/ThemeContext.tsx | 232 ++- src/renderer/src/index.css | 221 +-- src/renderer/src/routes/home.tsx | 82 +- src/renderer/src/styles/fonts.css | 98 +- src/renderer/src/styles/themes.css | 1246 +++++++++++++---- 14 files changed, 2050 insertions(+), 588 deletions(-) create mode 100644 src/renderer/src/components/theme/ModeToggle.tsx create mode 100644 src/renderer/src/components/theme/ThemeDropdown.tsx diff --git a/package.json b/package.json index 8b0c7b5..47946ba 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,11 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@fontsource-variable/nunito": "^5.2.7", + "@fontsource-variable/source-sans-3": "^5.2.9", + "@fontsource/vt323": "^5.2.7", "@hookform/resolvers": "^3.9.1", "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-accordion": "^1.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06cbff0..83c28d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,21 @@ importers: '@electron-toolkit/utils': specifier: ^3.0.0 version: 3.0.0(electron@33.4.11) + '@fontsource-variable/inter': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource-variable/jetbrains-mono': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource-variable/nunito': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource-variable/source-sans-3': + specifier: ^5.2.9 + version: 5.2.9 + '@fontsource/vt323': + specifier: ^5.2.7 + version: 5.2.7 '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.71.1(react@18.3.1)) @@ -820,6 +835,21 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + + '@fontsource-variable/jetbrains-mono@5.2.8': + resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==} + + '@fontsource-variable/nunito@5.2.7': + resolution: {integrity: sha512-2N8QhatkyKgSUbAGZO2FYLioxA32+RyI1EplVLawbpkGjUeui9Qg9VMrpkCaik1ydjFjfLV+kzQ0cGEsMrMenQ==} + + '@fontsource-variable/source-sans-3@5.2.9': + resolution: {integrity: sha512-K9fkQbb0BNnRmmaU+Gpr6U4IaYKpNzA8t07J6aam2i+6v84RT72MVpar8OBr3yYAEznj4riKivPQ2nAF2fPKsw==} + + '@fontsource/vt323@5.2.7': + resolution: {integrity: sha512-8JTMM23vMhQxin9Cn/ijty8cNwXW4INrln0VAJ2227Rz0CVfkzM3qr3l/CqudZJ6BXCnbCGUTdf2ym3cTNex8A==} + '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -5133,6 +5163,16 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@fontsource-variable/inter@5.2.8': {} + + '@fontsource-variable/jetbrains-mono@5.2.8': {} + + '@fontsource-variable/nunito@5.2.7': {} + + '@fontsource-variable/source-sans-3@5.2.9': {} + + '@fontsource/vt323@5.2.7': {} + '@gar/promisify@1.1.3': {} '@hookform/resolvers@3.10.0(react-hook-form@7.71.1(react@18.3.1))': diff --git a/src/renderer/src/components/layout/Header.tsx b/src/renderer/src/components/layout/Header.tsx index cde4113..e744706 100644 --- a/src/renderer/src/components/layout/Header.tsx +++ b/src/renderer/src/components/layout/Header.tsx @@ -1,23 +1,25 @@ -import { Moon, Sun, Search } from 'lucide-react' +import { Search } from 'lucide-react' import { Button } from '@renderer/components/ui/button' -import { useTheme } from '@renderer/contexts/ThemeContext' import { useSettings } from '@renderer/contexts/SettingsContext' import { UpdateIndicator } from '@renderer/components/update/UpdateIndicator' +import { ThemeDropdown } from '@renderer/components/theme/ThemeDropdown' +import { ModeToggle } from '@renderer/components/theme/ModeToggle' interface HeaderProps { onSearchClick?: () => void } export function Header({ onSearchClick }: HeaderProps) { - const { theme, toggleMode } = useTheme() const { appVersion, settings } = useSettings() - const isDark = theme.mode === 'dark' const hospitalName = settings?.['hospital'] || '' const unitName = settings?.['unit'] || '' return ( -
    +
    + {/* Gradient border bottom */} +
    + {/* Left: Hospital & Department in two rows */}
    {(hospitalName || unitName) ? ( @@ -35,7 +37,7 @@ export function Header({ onSearchClick }: HeaderProps) { {/* Center: Search trigger */} {/* Right: Actions */} -
    +
    - + {/* Theme dropdown for switching themes */} + + + {/* Animated light/dark toggle */} + {appVersion && ( - v{appVersion} + v{appVersion} )}
    diff --git a/src/renderer/src/components/layout/Sidebar.tsx b/src/renderer/src/components/layout/Sidebar.tsx index 5038e9b..2a3022e 100644 --- a/src/renderer/src/components/layout/Sidebar.tsx +++ b/src/renderer/src/components/layout/Sidebar.tsx @@ -14,24 +14,36 @@ interface NavItemProps { icon: React.ReactNode label: string end?: boolean + index?: number } -function NavItem({ to, icon, label, end }: NavItemProps) { +function NavItem({ to, icon, label, end, index = 0 }: NavItemProps) { return ( cn( - 'flex flex-col items-center justify-center gap-1.5 w-full py-3 rounded-xl transition-all duration-150', + 'group relative flex flex-col items-center justify-center gap-1.5 w-full py-3 rounded-xl transition-all duration-200 animate-fade-in-up', isActive - ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm' + ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-theme-md' : 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground' ) } + style={{ animationDelay: `${index * 50}ms` }} > - {icon} - {label} + {({ isActive }) => ( + <> + {/* Active indicator bar */} + {isActive && ( +
    + )} +
    + {icon} +
    + {label} + + )} ) } @@ -45,22 +57,27 @@ export function Sidebar() { ] return ( -
    diff --git a/src/renderer/src/components/theme/ModeToggle.tsx b/src/renderer/src/components/theme/ModeToggle.tsx new file mode 100644 index 0000000..a464d6f --- /dev/null +++ b/src/renderer/src/components/theme/ModeToggle.tsx @@ -0,0 +1,116 @@ +import { useTheme } from '@renderer/contexts/ThemeContext' +import { cn } from '@renderer/lib/utils' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@renderer/components/ui/tooltip' + +/** + * Animated sun/moon mode toggle with smooth transitions + * Features orbital animation and morphing icons + */ +export function ModeToggle() { + const { theme, toggleMode } = useTheme() + const isDark = theme.mode === 'dark' + + return ( + + + + + + +

    Switch to {isDark ? 'light' : 'dark'} mode

    +
    +
    +
    + ) +} diff --git a/src/renderer/src/components/theme/ThemeDropdown.tsx b/src/renderer/src/components/theme/ThemeDropdown.tsx new file mode 100644 index 0000000..42f7c4d --- /dev/null +++ b/src/renderer/src/components/theme/ThemeDropdown.tsx @@ -0,0 +1,283 @@ +import { useTheme, type ThemeId, type ThemeBase } from '@renderer/contexts/ThemeContext' +import { cn } from '@renderer/lib/utils' +import { Check, Sparkles } from 'lucide-react' +import { Button } from '@renderer/components/ui/button' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@renderer/components/ui/popover' +import { useState } from 'react' + +// Theme visual data with gradients and preview colors +const themeVisuals: Record< + ThemeBase, + { + gradient: string + preview: { bg: string; accent: string; text: string } + icon: string + } +> = { + aurora: { + gradient: 'from-cyan-400 via-teal-500 to-emerald-500', + preview: { bg: '#f5f9fa', accent: '#2d9ea8', text: '#1a3035' }, + icon: '🌊' + }, + meadow: { + gradient: 'from-green-400 via-emerald-500 to-teal-600', + preview: { bg: '#f4f8f5', accent: '#4a9960', text: '#1a2820' }, + icon: '🌿' + }, + bloom: { + gradient: 'from-pink-300 via-rose-400 to-pink-500', + preview: { bg: '#faf5f7', accent: '#d87a9e', text: '#2a1820' }, + icon: '🌸' + }, + latte: { + gradient: 'from-amber-300 via-orange-400 to-amber-600', + preview: { bg: '#f8f4f0', accent: '#8a6a4a', text: '#2a2018' }, + icon: '☕' + }, + slate: { + gradient: 'from-slate-400 via-gray-500 to-zinc-600', + preview: { bg: '#f9f9fa', accent: '#3a3d45', text: '#1a1a1c' }, + icon: '🔲' + }, + midnight: { + gradient: 'from-purple-500 via-violet-600 to-indigo-700', + preview: { bg: '#f8f5fa', accent: '#a87ce8', text: '#2a1830' }, + icon: '🌙' + }, + obsidian: { + gradient: 'from-zinc-400 via-neutral-600 to-stone-800', + preview: { bg: '#000000', accent: '#ffffff', text: '#a0a0a0' }, + icon: '⬛' + }, + ocean: { + gradient: 'from-blue-400 via-indigo-500 to-blue-600', + preview: { bg: '#f0f5fa', accent: '#2a6ab8', text: '#1a2530' }, + icon: '🌊' + }, + retro: { + gradient: 'from-amber-400 via-yellow-500 to-green-500', + preview: { bg: '#1a1408', accent: '#4aff4a', text: '#ffcc00' }, + icon: '📺' + } +} + +const themeLabels: Record = { + aurora: { name: 'Aurora', tagline: 'Modern & crisp' }, + meadow: { name: 'Meadow', tagline: 'Fresh & natural' }, + bloom: { name: 'Bloom', tagline: 'Soft & gentle' }, + latte: { name: 'Latte', tagline: 'Warm & cozy' }, + slate: { name: 'Slate', tagline: 'Sharp & clean' }, + midnight: { name: 'Midnight', tagline: 'Purple elegance' }, + obsidian: { name: 'Obsidian', tagline: 'True black AMOLED' }, + ocean: { name: 'Ocean', tagline: 'Calm & professional' }, + retro: { name: 'Retro', tagline: 'CRT nostalgia' } +} + +interface ThemeCardProps { + base: ThemeBase + isSelected: boolean + currentMode: 'light' | 'dark' + onSelect: (themeId: ThemeId) => void +} + +function ThemeCard({ base, isSelected, currentMode, onSelect }: ThemeCardProps) { + const visuals = themeVisuals[base] + const labels = themeLabels[base] + const themeId = `${base}-${currentMode}` as ThemeId + + return ( + + ) +} + +export function ThemeDropdown() { + const { theme, setTheme, toggleMode } = useTheme() + const [open, setOpen] = useState(false) + const isDark = theme.mode === 'dark' + const currentVisuals = themeVisuals[theme.base] + + // All 9 themes + const allThemes: ThemeBase[] = ['aurora', 'meadow', 'bloom', 'latte', 'slate', 'midnight', 'obsidian', 'ocean', 'retro'] + + return ( + + + + + + {/* Header */} +
    +
    +

    Theme

    +

    + Choose your visual style +

    +
    + {/* Mode toggle pill */} +
    + + +
    +
    + + {/* Theme grid - 3x3 for 9 themes */} +
    + {allThemes.map((base) => { + const isCurrentBase = theme.base === base + return ( + { + setTheme(id) + setOpen(false) + }} + /> + ) + })} +
    +
    +
    + ) +} diff --git a/src/renderer/src/components/theme/ThemeSwitcher.tsx b/src/renderer/src/components/theme/ThemeSwitcher.tsx index ff5c77d..0b6b14c 100644 --- a/src/renderer/src/components/theme/ThemeSwitcher.tsx +++ b/src/renderer/src/components/theme/ThemeSwitcher.tsx @@ -1,8 +1,63 @@ import { useTheme, themes, type ThemeId, type ThemeBase } from '@renderer/contexts/ThemeContext' import { cn } from '@renderer/lib/utils' -import { Check, Moon, Sun } from 'lucide-react' +import { Check, Moon, Sun, Monitor } from 'lucide-react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@renderer/components/ui/card' +// Theme preview colors matching the actual theme CSS +const themePreviewColors: Record = { + aurora: { + light: { bg: '#f5f9fa', card: '#ffffff', primary: '#2d9ea8', accent: '#e67a5a', border: '#d8e8eb', muted: '#e8f2f4', text: '#1a3035' }, + dark: { bg: '#1a2428', card: '#1f2a30', primary: '#4dc8d2', accent: '#f09080', border: '#2a3840', muted: '#243038', text: '#e8f4f6' } + }, + meadow: { + light: { bg: '#f4f8f5', card: '#fafdfb', primary: '#4a9960', accent: '#c9a84c', border: '#d8e8dc', muted: '#e4f0e7', text: '#1a2820' }, + dark: { bg: '#1a2420', card: '#1f2a24', primary: '#6ab87a', accent: '#ddc06c', border: '#2a3830', muted: '#243028', text: '#e4f4e8' } + }, + bloom: { + light: { bg: '#faf5f7', card: '#fefcfd', primary: '#d87a9e', accent: '#e8b090', border: '#f0dfe5', muted: '#f8e8ee', text: '#2a1820' }, + dark: { bg: '#221a20', card: '#281f26', primary: '#e8a0b8', accent: '#f0c8b0', border: '#382832', muted: '#302028', text: '#f4e4ec' } + }, + latte: { + light: { bg: '#f8f4f0', card: '#fdfbf9', primary: '#8a6a4a', accent: '#c8a060', border: '#e8dcd0', muted: '#f0e8e0', text: '#2a2018' }, + dark: { bg: '#221c18', card: '#28221e', primary: '#c8a080', accent: '#e0c090', border: '#382e28', muted: '#302820', text: '#f4ece4' } + }, + slate: { + light: { bg: '#f9f9fa', card: '#ffffff', primary: '#3a3d45', accent: '#5a8ec8', border: '#e4e4e8', muted: '#eeeef0', text: '#1a1a1c' }, + dark: { bg: '#1a1a1c', card: '#222224', primary: '#e8e8ec', accent: '#6aa0d8', border: '#32323a', muted: '#2a2a30', text: '#f0f0f2' } + }, + midnight: { + light: { bg: '#f8f5fa', card: '#fefcff', primary: '#8a5cc8', accent: '#6040a0', border: '#e8dff0', muted: '#f0e8f5', text: '#2a1838' }, + dark: { bg: '#0d0810', card: '#18121a', primary: '#a87ce8', accent: '#7a5ac0', border: '#2a2030', muted: '#221828', text: '#e8e0f0' } + }, + obsidian: { + light: { bg: '#fafafa', card: '#ffffff', primary: '#1a1a1a', accent: '#404040', border: '#e0e0e0', muted: '#f0f0f0', text: '#1a1a1a' }, + dark: { bg: '#000000', card: '#0a0a0a', primary: '#ffffff', accent: '#808080', border: '#1a1a1a', muted: '#121212', text: '#f0f0f0' } + }, + ocean: { + light: { bg: '#f0f5fa', card: '#fafcff', primary: '#2a6ab8', accent: '#1a4a90', border: '#d0e0f0', muted: '#e0ecf5', text: '#1a2530' }, + dark: { bg: '#0a1420', card: '#101a28', primary: '#4a90d8', accent: '#3070b8', border: '#1a2a40', muted: '#142030', text: '#e0ecf5' } + }, + retro: { + light: { bg: '#f5f0e8', card: '#faf8f4', primary: '#4a8a4a', accent: '#c8a040', border: '#d8d0c0', muted: '#e8e0d4', text: '#2a2818' }, + dark: { bg: '#1a1408', card: '#201a10', primary: '#4aff4a', accent: '#ffcc00', border: '#3a3020', muted: '#282018', text: '#e0c880' } + } +} + +const themeMetadata: Record = { + aurora: { icon: '🌊', name: 'Aurora', description: 'Modern teal with coral accents' }, + meadow: { icon: '🌿', name: 'Meadow', description: 'Fresh green, earthy and calming' }, + bloom: { icon: '🌸', name: 'Bloom', description: 'Soft pastel rose, gentle and airy' }, + latte: { icon: '☕', name: 'Latte', description: 'Warm coffee browns, cozy and inviting' }, + slate: { icon: '🔲', name: 'Slate', description: 'Sharp grays, professional and clean' }, + midnight: { icon: '🌙', name: 'Midnight', description: 'Elegant purple, sophisticated vibes' }, + obsidian: { icon: '⬛', name: 'Obsidian', description: 'True black AMOLED, minimal' }, + ocean: { icon: '🌊', name: 'Ocean', description: 'Deep blue, calm and professional' }, + retro: { icon: '📺', name: 'Retro', description: 'CRT terminal nostalgia, vintage feel' } +} + interface ThemePreviewProps { themeId: ThemeId isSelected: boolean @@ -14,90 +69,137 @@ function ThemePreview({ themeId, isSelected, onClick }: ThemePreviewProps) { if (!theme) return null const isDark = theme.mode === 'dark' + const colors = isDark + ? themePreviewColors[theme.base].dark + : themePreviewColors[theme.base].light + const meta = themeMetadata[theme.base] return ( ) } interface ThemeGroupProps { base: ThemeBase - label: string - description: string } -function ThemeGroup({ base, label, description }: ThemeGroupProps) { +function ThemeGroup({ base }: ThemeGroupProps) { const { themeId, setTheme } = useTheme() const groupThemes = themes.filter((t) => t.base === base) + const meta = themeMetadata[base] return (
    -
    -

    {label}

    -

    {description}

    +
    + {meta.icon} +
    +

    {meta.name}

    +

    {meta.description}

    +
    -
    +
    {groupThemes.map((theme) => ( - Theme - - Choose your preferred theme. Each theme has light and dark variants. - +
    +
    + +
    +
    + Theme + + Choose your visual style. Each theme has unique colors, typography, and character. + +
    +
    - - - - - + + {allThemes.map((base) => ( + + ))} ) diff --git a/src/renderer/src/components/ui/button.tsx b/src/renderer/src/components/ui/button.tsx index 141ea07..a05a4b4 100644 --- a/src/renderer/src/components/ui/button.tsx +++ b/src/renderer/src/components/ui/button.tsx @@ -5,16 +5,18 @@ import { cn } from '@/lib/utils' import { Loader2 } from 'lucide-react' const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', + default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-theme-sm hover:shadow-theme-md', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', + gradient: + 'bg-gradient-primary text-primary-foreground shadow-theme-primary hover:shadow-theme-lg transition-all duration-300 hover:scale-[1.02]', expandIcon: 'group relative text-primary-foreground bg-primary hover:bg-primary/90', ringHover: 'bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2', diff --git a/src/renderer/src/components/ui/card.tsx b/src/renderer/src/components/ui/card.tsx index a014766..1cf1b86 100644 --- a/src/renderer/src/components/ui/card.tsx +++ b/src/renderer/src/components/ui/card.tsx @@ -2,11 +2,19 @@ import * as React from 'react' import { cn } from '@/lib/utils' -const Card = React.forwardRef>( - ({ className, ...props }, ref) => ( +export interface CardProps extends React.HTMLAttributes { + hoverable?: boolean +} + +const Card = React.forwardRef( + ({ className, hoverable = false, ...props }, ref) => (
    ) diff --git a/src/renderer/src/contexts/ThemeContext.tsx b/src/renderer/src/contexts/ThemeContext.tsx index 343f903..14beb51 100644 --- a/src/renderer/src/contexts/ThemeContext.tsx +++ b/src/renderer/src/contexts/ThemeContext.tsx @@ -1,16 +1,26 @@ import React, { createContext, useContext, useEffect, useState, useCallback } from 'react' export type ThemeId = - | 'default-light' - | 'default-dark' - | 'medical-light' - | 'medical-dark' - | 'classic-light' - | 'classic-dark' - | 'minimal-light' - | 'minimal-dark' - -export type ThemeBase = 'default' | 'medical' | 'classic' | 'minimal' + | 'aurora-light' + | 'aurora-dark' + | 'meadow-light' + | 'meadow-dark' + | 'bloom-light' + | 'bloom-dark' + | 'latte-light' + | 'latte-dark' + | 'slate-light' + | 'slate-dark' + | 'midnight-light' + | 'midnight-dark' + | 'obsidian-light' + | 'obsidian-dark' + | 'ocean-light' + | 'ocean-dark' + | 'retro-light' + | 'retro-dark' + +export type ThemeBase = 'aurora' | 'meadow' | 'bloom' | 'latte' | 'slate' | 'midnight' | 'obsidian' | 'ocean' | 'retro' export type ThemeMode = 'light' | 'dark' export interface Theme { @@ -24,68 +34,148 @@ export interface Theme { export const themes: Theme[] = [ { - id: 'default-light', - name: 'Default Light', - base: 'default', + id: 'aurora-light', + name: 'Aurora', + base: 'aurora', mode: 'light', fontFamily: 'Inter', - description: 'Clean and modern with blue accents' + description: 'Modern teal with coral accents' }, { - id: 'default-dark', - name: 'Default Dark', - base: 'default', + id: 'aurora-dark', + name: 'Aurora', + base: 'aurora', mode: 'dark', fontFamily: 'Inter', - description: 'Dark blue-gray palette' + description: 'Modern teal with coral accents' + }, + { + id: 'meadow-light', + name: 'Meadow', + base: 'meadow', + mode: 'light', + fontFamily: 'Source Sans 3', + description: 'Fresh green, earthy and calming' + }, + { + id: 'meadow-dark', + name: 'Meadow', + base: 'meadow', + mode: 'dark', + fontFamily: 'Source Sans 3', + description: 'Fresh green, earthy and calming' }, { - id: 'medical-light', - name: 'Medical Light', - base: 'medical', + id: 'bloom-light', + name: 'Bloom', + base: 'bloom', mode: 'light', - fontFamily: 'IBM Plex Sans', - description: 'Professional clinical appearance' + fontFamily: 'Inter', + description: 'Soft pastel rose, gentle and airy' }, { - id: 'medical-dark', - name: 'Medical Dark', - base: 'medical', + id: 'bloom-dark', + name: 'Bloom', + base: 'bloom', mode: 'dark', - fontFamily: 'IBM Plex Sans', - description: 'Deep blue-green dark mode' + fontFamily: 'Inter', + description: 'Soft pastel rose, gentle and airy' }, { - id: 'classic-light', - name: 'Classic Light', - base: 'classic', + id: 'latte-light', + name: 'Latte', + base: 'latte', mode: 'light', - fontFamily: 'Source Serif 4', - description: 'Warm paper-like background' + fontFamily: 'Source Sans 3', + description: 'Warm coffee browns, cozy and inviting' }, { - id: 'classic-dark', - name: 'Classic Dark', - base: 'classic', + id: 'latte-dark', + name: 'Latte', + base: 'latte', mode: 'dark', - fontFamily: 'Source Serif 4', - description: 'Warm dark tones' + fontFamily: 'Source Sans 3', + description: 'Warm coffee browns, cozy and inviting' }, { - id: 'minimal-light', - name: 'Minimal Light', - base: 'minimal', + id: 'slate-light', + name: 'Slate', + base: 'slate', mode: 'light', - fontFamily: 'Space Grotesk', - description: 'Pure white, sharp contrasts' + fontFamily: 'Nunito', + description: 'Sharp grays, professional and clean' }, { - id: 'minimal-dark', - name: 'Minimal Dark', - base: 'minimal', + id: 'slate-dark', + name: 'Slate', + base: 'slate', mode: 'dark', - fontFamily: 'Space Grotesk', - description: 'True black, high contrast' + fontFamily: 'Nunito', + description: 'Sharp grays, professional and clean' + }, + { + id: 'midnight-light', + name: 'Midnight', + base: 'midnight', + mode: 'light', + fontFamily: 'Nunito', + description: 'Elegant lavender with purple accents' + }, + { + id: 'midnight-dark', + name: 'Midnight', + base: 'midnight', + mode: 'dark', + fontFamily: 'Nunito', + description: 'Deep purple, elegant OLED vibes' + }, + { + id: 'obsidian-light', + name: 'Obsidian', + base: 'obsidian', + mode: 'light', + fontFamily: 'Nunito', + description: 'Pure white with black accents' + }, + { + id: 'obsidian-dark', + name: 'Obsidian', + base: 'obsidian', + mode: 'dark', + fontFamily: 'Nunito', + description: 'True black AMOLED, minimal' + }, + { + id: 'ocean-light', + name: 'Ocean', + base: 'ocean', + mode: 'light', + fontFamily: 'Inter', + description: 'Deep blue, calm and professional' + }, + { + id: 'ocean-dark', + name: 'Ocean', + base: 'ocean', + mode: 'dark', + fontFamily: 'Inter', + description: 'Deep blue, calm and professional' + }, + { + id: 'retro-light', + name: 'Retro', + base: 'retro', + mode: 'light', + fontFamily: 'VT323', + description: 'Vintage terminal, paper-tape era' + }, + { + id: 'retro-dark', + name: 'Retro', + base: 'retro', + mode: 'dark', + fontFamily: 'VT323', + description: 'CRT phosphor glow, amber on black' } ] @@ -102,6 +192,21 @@ const ThemeContext = createContext(undefined) const THEME_STORAGE_KEY = 'app_theme' +// Migration map for old theme IDs to new ones +const themeMapping: Record = { + // Legacy v1 themes + 'default-light': 'ocean-light', + 'default-dark': 'ocean-dark', + 'medical-light': 'meadow-light', + 'medical-dark': 'meadow-dark', + 'classic-light': 'bloom-light', + 'classic-dark': 'bloom-dark', + 'minimal-light': 'slate-light', + 'minimal-dark': 'slate-dark', + // Legacy single midnight theme + 'midnight': 'midnight-dark' +} + function getSystemPrefersDark(): boolean { if (typeof window !== 'undefined' && window.matchMedia) { return window.matchMedia('(prefers-color-scheme: dark)').matches @@ -112,15 +217,25 @@ function getSystemPrefersDark(): boolean { function getStoredTheme(): ThemeId | null { if (typeof window !== 'undefined') { const stored = localStorage.getItem(THEME_STORAGE_KEY) - if (stored && themes.some((t) => t.id === stored)) { - return stored as ThemeId + if (stored) { + // Check if it's a new theme ID + if (themes.some((t) => t.id === stored)) { + return stored as ThemeId + } + // Check if it's an old theme ID that needs migration + if (stored in themeMapping) { + const migratedTheme = themeMapping[stored] + // Save the migrated theme + localStorage.setItem(THEME_STORAGE_KEY, migratedTheme) + return migratedTheme + } } } return null } function getDefaultTheme(systemDark: boolean): ThemeId { - return systemDark ? 'default-dark' : 'default-light' + return systemDark ? 'ocean-dark' : 'ocean-light' } export function ThemeProvider({ children }: { children: React.ReactNode }) { @@ -190,7 +305,7 @@ export function useTheme() { // Helper to get the opposite mode theme export function getOppositeTheme(themeId: ThemeId): ThemeId { const theme = themes.find((t) => t.id === themeId) - if (!theme) return 'default-light' + if (!theme) return 'ocean-dark' const newMode = theme.mode === 'light' ? 'dark' : 'light' return `${theme.base}-${newMode}` as ThemeId } @@ -198,9 +313,14 @@ export function getOppositeTheme(themeId: ThemeId): ThemeId { // Helper to get themes grouped by base export function getThemesByBase(): Record { return { - default: themes.filter((t) => t.base === 'default'), - medical: themes.filter((t) => t.base === 'medical'), - classic: themes.filter((t) => t.base === 'classic'), - minimal: themes.filter((t) => t.base === 'minimal') + aurora: themes.filter((t) => t.base === 'aurora'), + meadow: themes.filter((t) => t.base === 'meadow'), + bloom: themes.filter((t) => t.base === 'bloom'), + latte: themes.filter((t) => t.base === 'latte'), + slate: themes.filter((t) => t.base === 'slate'), + midnight: themes.filter((t) => t.base === 'midnight'), + obsidian: themes.filter((t) => t.base === 'obsidian'), + ocean: themes.filter((t) => t.base === 'ocean'), + retro: themes.filter((t) => t.base === 'retro') } } diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index f9aae71..c01f6bd 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -1,7 +1,7 @@ @import 'tailwindcss'; /* Custom dark mode variant for data-theme attribute - matches all dark theme variants */ -@custom-variant dark (&:where([data-theme=dark], [data-theme=default-dark], [data-theme=medical-dark], [data-theme=classic-dark], [data-theme=minimal-dark], [data-theme=dark] *, [data-theme=default-dark] *, [data-theme=medical-dark] *, [data-theme=classic-dark] *, [data-theme=minimal-dark] *)); +@custom-variant dark (&:where([data-theme=aurora-dark], [data-theme=meadow-dark], [data-theme=bloom-dark], [data-theme=latte-dark], [data-theme=slate-dark], [data-theme=midnight-dark], [data-theme=obsidian-dark], [data-theme=ocean-dark], [data-theme=retro-dark], [data-theme=aurora-dark] *, [data-theme=meadow-dark] *, [data-theme=bloom-dark] *, [data-theme=latte-dark] *, [data-theme=slate-dark] *, [data-theme=midnight-dark] *, [data-theme=obsidian-dark] *, [data-theme=ocean-dark] *, [data-theme=retro-dark] *)); /* Source paths for class detection - relative to this CSS file */ @source "."; @@ -13,7 +13,8 @@ /* Custom theme configuration */ @theme { /* Font families - will be extended by theme variants */ - --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; + --font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; /* Border radius */ --radius-lg: 0.5rem; @@ -131,103 +132,74 @@ } --animate-stagger-fade-in: stagger-fade-in 0.3s ease-out both; -} - -/* Base semantic colors using CSS custom properties */ -/* These will be overridden by theme variants */ -:root { - /* Light theme defaults */ - --background: oklch(100% 0 0); - --foreground: oklch(14.1% 0.02 264.7); - - --card: oklch(100% 0 0); - --card-foreground: oklch(14.1% 0.02 264.7); - - --popover: oklch(100% 0 0); - --popover-foreground: oklch(14.1% 0.02 264.7); - - --primary: oklch(54.6% 0.245 262.9); - --primary-foreground: oklch(97.1% 0.01 264.7); - - --secondary: oklch(96.7% 0.01 264.7); - --secondary-foreground: oklch(21% 0.02 264.7); - - --muted: oklch(96.7% 0.01 264.7); - --muted-foreground: oklch(55.6% 0.02 264.7); - - --accent: oklch(96.7% 0.01 264.7); - --accent-foreground: oklch(21% 0.02 264.7); - - --destructive: oklch(57.7% 0.215 27.3); - --destructive-foreground: oklch(97.1% 0.01 264.7); - - --border: oklch(91.4% 0.01 264.7); - --input: oklch(91.4% 0.01 264.7); - --ring: oklch(54.6% 0.245 262.9); - --chart-1: oklch(62.8% 0.19 259); - --chart-2: oklch(63.2% 0.172 165); - --chart-3: oklch(47% 0.12 264.7); - --chart-4: oklch(75.9% 0.154 89); - --chart-5: oklch(66.9% 0.202 41); - - /* Sidebar colors */ - --sidebar-background: oklch(98.5% 0.005 264.7); - --sidebar-foreground: oklch(37% 0.02 264.7); - --sidebar-primary: oklch(54.6% 0.245 262.9); - --sidebar-primary-foreground: oklch(97.1% 0.01 264.7); - --sidebar-accent: oklch(95% 0.01 264.7); - --sidebar-accent-foreground: oklch(21% 0.02 264.7); - --sidebar-border: oklch(91.4% 0.01 264.7); - --sidebar-ring: oklch(54.6% 0.245 262.9); -} - -/* Dark theme */ -:root[data-theme='dark'], -[data-theme='default-dark'] { - --background: oklch(14.5% 0.035 264.7); - --foreground: oklch(97.1% 0.01 264.7); - - --card: oklch(14.5% 0.035 264.7); - --card-foreground: oklch(97.1% 0.01 264.7); - - --popover: oklch(14.5% 0.035 264.7); - --popover-foreground: oklch(97.1% 0.01 264.7); - - --primary: oklch(62.8% 0.19 259); - --primary-foreground: oklch(21% 0.02 264.7); - - --secondary: oklch(25% 0.04 264.7); - --secondary-foreground: oklch(97.1% 0.01 264.7); + /* New entrance animations */ + @keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } + } - --muted: oklch(25% 0.04 264.7); - --muted-foreground: oklch(71.1% 0.02 264.7); + @keyframes scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } + } - --accent: oklch(25% 0.04 264.7); - --accent-foreground: oklch(97.1% 0.01 264.7); + @keyframes slide-in-right { + from { + opacity: 0; + transform: translateX(16px); + } + to { + opacity: 1; + transform: translateX(0); + } + } - --destructive: oklch(45% 0.18 27.3); - --destructive-foreground: oklch(97.1% 0.01 264.7); + @keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } + } - --border: oklch(25% 0.04 264.7); - --input: oklch(25% 0.04 264.7); - --ring: oklch(56% 0.2 259); + @keyframes pulse-soft { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } + } - --chart-1: oklch(62.8% 0.19 259); - --chart-2: oklch(63.2% 0.172 165); - --chart-3: oklch(72% 0.15 264.7); - --chart-4: oklch(75.9% 0.154 89); - --chart-5: oklch(66.9% 0.202 41); + @keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-4px); + } + } - /* Sidebar colors - dark */ - --sidebar-background: oklch(17% 0.04 264.7); - --sidebar-foreground: oklch(75% 0.02 264.7); - --sidebar-primary: oklch(62.8% 0.19 259); - --sidebar-primary-foreground: oklch(21% 0.02 264.7); - --sidebar-accent: oklch(22% 0.035 264.7); - --sidebar-accent-foreground: oklch(97.1% 0.01 264.7); - --sidebar-border: oklch(25% 0.04 264.7); - --sidebar-ring: oklch(56% 0.2 259); + --animate-fade-in-up: fade-in-up 0.4s ease-out both; + --animate-scale-in: scale-in 0.3s ease-out both; + --animate-slide-in-right: slide-in-right 0.35s ease-out both; + --animate-shimmer: shimmer 2s linear infinite; + --animate-pulse-soft: pulse-soft 2s ease-in-out infinite; + --animate-float: float 3s ease-in-out infinite; } /* Extend theme with semantic color utilities */ @@ -557,6 +529,65 @@ .group:hover .group-hover\:text-sidebar-foreground { color: var(--sidebar-foreground); } + + /* Hover lift effect */ + .hover-lift { + transition: transform 0.2s ease, box-shadow 0.2s ease; + } + .hover-lift:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + } + + /* Staggered animation delays */ + .animate-stagger-1 { animation-delay: 50ms; } + .animate-stagger-2 { animation-delay: 100ms; } + .animate-stagger-3 { animation-delay: 150ms; } + .animate-stagger-4 { animation-delay: 200ms; } + .animate-stagger-5 { animation-delay: 250ms; } + + /* Theme-aware shadow utilities */ + .shadow-theme-sm { box-shadow: var(--shadow-sm); } + .shadow-theme-md { box-shadow: var(--shadow-md); } + .shadow-theme-lg { box-shadow: var(--shadow-lg); } + .shadow-theme-primary { box-shadow: var(--shadow-primary); } + + /* Gradient utilities */ + .bg-gradient-primary { background: var(--gradient-primary); } + .bg-gradient-radial { background: var(--gradient-bg-radial); } + .bg-gradient-shine { + background: var(--gradient-card-shine); + background-size: 200% 100%; + } + + /* Spring/bounce transitions */ + .transition-spring { + transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); + } + .transition-bounce { + transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); + } + + /* Animation utilities */ + .animate-fade-in-up { + animation: fade-in-up 0.4s ease-out both; + } + .animate-scale-in { + animation: scale-in 0.3s ease-out both; + } + .animate-slide-in-right { + animation: slide-in-right 0.35s ease-out both; + } + .animate-shimmer { + animation: shimmer 2s linear infinite; + background-size: 200% 100%; + } + .animate-pulse-soft { + animation: pulse-soft 2s ease-in-out infinite; + } + .animate-float { + animation: float 3s ease-in-out infinite; + } } /* Base layer */ @@ -565,6 +596,12 @@ @apply border-border; } + html { + /* GPU-accelerated font smoothing for crisp text rendering */ + @apply antialiased; + text-rendering: optimizeLegibility; + } + body { @apply bg-background text-foreground; font-family: var(--font-sans); diff --git a/src/renderer/src/routes/home.tsx b/src/renderer/src/routes/home.tsx index 62f88c4..024c19b 100644 --- a/src/renderer/src/routes/home.tsx +++ b/src/renderer/src/routes/home.tsx @@ -47,9 +47,11 @@ function GreetingHeader() { const iconColor = hour < 12 ? 'text-amber-500' : hour < 17 ? 'text-orange-500' : 'text-indigo-400' return ( -
    +
    - +
    + +
    {greeting}
    {format(new Date(), 'EEEE, MMMM d')} @@ -76,28 +78,41 @@ function StatsCards({ stats, isLoading }: StatsCardsProps) { value: stats?.totalPatients ?? 0, label: 'Total Patients', change: stats?.patientsThisMonth || 0, - changeLabel: 'this month' + changeLabel: 'this month', + color: 'text-blue-500', + bgColor: 'bg-blue-500/10' }, { icon: Stethoscope, value: stats?.totalSurgeries ?? 0, label: 'Surgeries', change: stats?.surgeriesThisMonth || 0, - changeLabel: 'this month' + changeLabel: 'this month', + color: 'text-emerald-500', + bgColor: 'bg-emerald-500/10' }, { icon: UserCog, value: stats?.totalDoctors ?? 0, label: 'Doctors', change: null, - changeLabel: 'registered' + changeLabel: 'registered', + color: 'text-violet-500', + bgColor: 'bg-violet-500/10' } ] return (
    {items.map((item, i) => ( - +
    @@ -119,13 +134,13 @@ function StatsCards({ stats, isLoading }: StatsCardsProps) {

    )}
    -
    - +
    +
    {/* Decorative gradient accent */} -
    +
    ))}
    @@ -144,23 +159,27 @@ function AddPatientCard({ className }: AddPatientCardProps) { ) @@ -173,18 +192,20 @@ interface NavCardProps { title: string description: string className?: string + index?: number } -function NavCard({ to, icon: Icon, title, description, className }: NavCardProps) { +function NavCard({ to, icon: Icon, title, description, className, index = 0 }: NavCardProps) { const navigate = useNavigate() return ( @@ -333,6 +356,9 @@ export default function Home() { return (
    + {/* Subtle radial gradient background */} +
    + {/* Row 1: Greeting Header */} @@ -348,10 +374,10 @@ export default function Home() { {/* Quick Links - 2x2 grid */}
    - - - - + + + +
    diff --git a/src/renderer/src/styles/fonts.css b/src/renderer/src/styles/fonts.css index 4d64c9b..526cc5f 100644 --- a/src/renderer/src/styles/fonts.css +++ b/src/renderer/src/styles/fonts.css @@ -1,88 +1,28 @@ -/* Inter - Default theme font */ -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 100 900; - font-display: swap; - src: url('https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -/* IBM Plex Sans - Medical Professional theme font */ -@font-face { - font-family: 'IBM Plex Sans'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYXgKVElMYYaJe8bpLHnCwDKhdHeFaxOedc.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +/* ============================================ + Font Imports - Using @fontsource packages + Fonts are bundled via npm for offline use + ============================================ */ -@font-face { - font-family: 'IBM Plex Sans'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYX9KVElMYYaJe8bpLHnCwDKjSL9AIFsdP3pBms.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +/* Inter - Modern, clean sans-serif (Aurora, Bloom, Ocean, Obsidian themes) */ +@import '@fontsource-variable/inter'; -@font-face { - font-family: 'IBM Plex Sans'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYX9KVElMYYaJe8bpLHnCwDKjQ76AIFsdP3pBms.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +/* Source Sans 3 - Professional, readable (Meadow, Latte themes) */ +@import '@fontsource-variable/source-sans-3'; -@font-face { - font-family: 'IBM Plex Sans'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url('https://fonts.gstatic.com/s/ibmplexsans/v19/zYX9KVElMYYaJe8bpLHnCwDKjWr7AIFsdP3pBms.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +/* Nunito - Friendly, rounded (Slate, Midnight themes) */ +@import '@fontsource-variable/nunito'; -/* Source Serif Pro - Classic theme font */ -@font-face { - font-family: 'Source Serif 4'; - font-style: normal; - font-weight: 200 900; - font-display: swap; - src: url('https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +/* VT323 - Pixel/terminal aesthetic (Retro theme) */ +@import '@fontsource/vt323'; -/* Space Grotesk - Minimal theme font */ -@font-face { - font-family: 'Space Grotesk'; - font-style: normal; - font-weight: 300 700; - font-display: swap; - src: url('https://fonts.gstatic.com/s/spacegrotesk/v16/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj7oUXskPMBBSSJLm2E.woff2') - format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} +/* JetBrains Mono - Code/data displays across all themes */ +@import '@fontsource-variable/jetbrains-mono'; /* Font family variables for themes */ :root { - --font-inter: 'Inter', ui-sans-serif, system-ui, sans-serif; - --font-ibm-plex: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; - --font-source-serif: 'Source Serif 4', ui-serif, Georgia, serif; - --font-space-grotesk: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif; + --font-inter: 'Inter Variable', ui-sans-serif, system-ui, -apple-system, sans-serif; + --font-source: 'Source Sans 3 Variable', ui-sans-serif, system-ui, -apple-system, sans-serif; + --font-nunito: 'Nunito Variable', ui-sans-serif, system-ui, -apple-system, sans-serif; + --font-vt323: 'VT323', 'Courier New', monospace; + --font-mono: 'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; } diff --git a/src/renderer/src/styles/themes.css b/src/renderer/src/styles/themes.css index ebf27c3..f90f79e 100644 --- a/src/renderer/src/styles/themes.css +++ b/src/renderer/src/styles/themes.css @@ -1,346 +1,1120 @@ /* ============================================ - Theme Variants - 4 themes x 2 modes = 8 total + Theme System - Distinctive & Memorable Themes + Each theme has unique colors, typography, and border styling ============================================ */ -/* Default Theme - Light (Inter font) */ -[data-theme='default-light'], +/* ============================================ + AURORA - Modern & Clean + Teal/cyan with coral accents, crisp and professional + Font: Plus Jakarta Sans | Radius: Default | Borders: Clean + ============================================ */ + +[data-theme='aurora-light'], :root:not([data-theme]) { --font-family: var(--font-inter); + --radius-base: 0.5rem; + --border-width: 1px; - --background: oklch(100% 0 0); - --foreground: oklch(14.1% 0.02 264.7); + --background: oklch(98.5% 0.008 220); + --foreground: oklch(18% 0.02 220); --card: oklch(100% 0 0); - --card-foreground: oklch(14.1% 0.02 264.7); + --card-foreground: oklch(18% 0.02 220); --popover: oklch(100% 0 0); - --popover-foreground: oklch(14.1% 0.02 264.7); + --popover-foreground: oklch(18% 0.02 220); - --primary: oklch(54.6% 0.245 262.9); - --primary-foreground: oklch(97.1% 0.01 264.7); + --primary: oklch(55% 0.15 195); + --primary-foreground: oklch(98% 0.01 195); - --secondary: oklch(96.7% 0.01 264.7); - --secondary-foreground: oklch(21% 0.02 264.7); + --secondary: oklch(95% 0.015 220); + --secondary-foreground: oklch(25% 0.02 220); - --muted: oklch(96.7% 0.01 264.7); - --muted-foreground: oklch(55.6% 0.02 264.7); + --muted: oklch(95% 0.012 220); + --muted-foreground: oklch(45% 0.02 220); - --accent: oklch(96.7% 0.01 264.7); - --accent-foreground: oklch(21% 0.02 264.7); + --accent: oklch(95% 0.015 220); + --accent-foreground: oklch(25% 0.02 220); - --destructive: oklch(57.7% 0.215 27.3); - --destructive-foreground: oklch(97.1% 0.01 264.7); + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); - --border: oklch(91.4% 0.01 264.7); - --input: oklch(91.4% 0.01 264.7); - --ring: oklch(54.6% 0.245 262.9); + --border: oklch(90% 0.012 220); + --input: oklch(90% 0.012 220); + --ring: oklch(55% 0.15 195); - --sidebar-background: oklch(98.5% 0.005 264.7); - --sidebar-foreground: oklch(37% 0.02 264.7); - --sidebar-primary: oklch(54.6% 0.245 262.9); - --sidebar-primary-foreground: oklch(97.1% 0.01 264.7); - --sidebar-accent: oklch(95% 0.01 264.7); - --sidebar-accent-foreground: oklch(21% 0.02 264.7); - --sidebar-border: oklch(91.4% 0.01 264.7); - --sidebar-ring: oklch(54.6% 0.245 262.9); + --sidebar-background: oklch(97% 0.01 220); + --sidebar-foreground: oklch(35% 0.02 220); + --sidebar-primary: oklch(55% 0.15 195); + --sidebar-primary-foreground: oklch(98% 0.01 195); + --sidebar-accent: oklch(93% 0.015 220); + --sidebar-accent-foreground: oklch(25% 0.02 220); + --sidebar-border: oklch(90% 0.012 220); + --sidebar-ring: oklch(55% 0.15 195); + + --gradient-primary: linear-gradient(135deg, oklch(55% 0.15 195), oklch(58% 0.12 210)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(95% 0.02 195 / 0.3), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(20% 0.02 220 / 0.05); + --shadow-md: 0 4px 6px -1px oklch(20% 0.02 220 / 0.07), 0 2px 4px -2px oklch(20% 0.02 220 / 0.05); + --shadow-lg: 0 10px 15px -3px oklch(20% 0.02 220 / 0.08), 0 4px 6px -4px oklch(20% 0.02 220 / 0.05); + --shadow-primary: 0 4px 14px -3px oklch(55% 0.15 195 / 0.35); + + --chart-1: oklch(55% 0.15 195); + --chart-2: oklch(65% 0.15 25); + --chart-3: oklch(60% 0.12 280); + --chart-4: oklch(70% 0.14 140); + --chart-5: oklch(65% 0.16 45); } -/* Default Theme - Dark (Inter font) */ -[data-theme='default-dark'] { +[data-theme='aurora-dark'] { --font-family: var(--font-inter); + --radius-base: 0.5rem; + --border-width: 1px; - --background: oklch(14.5% 0.035 264.7); - --foreground: oklch(97.1% 0.01 264.7); + --background: oklch(14% 0.025 220); + --foreground: oklch(96% 0.01 220); - --card: oklch(17% 0.035 264.7); - --card-foreground: oklch(97.1% 0.01 264.7); + --card: oklch(17% 0.025 220); + --card-foreground: oklch(96% 0.01 220); - --popover: oklch(17% 0.035 264.7); - --popover-foreground: oklch(97.1% 0.01 264.7); + --popover: oklch(17% 0.025 220); + --popover-foreground: oklch(96% 0.01 220); - --primary: oklch(62.8% 0.19 259); - --primary-foreground: oklch(14% 0.02 264.7); + --primary: oklch(65% 0.14 195); + --primary-foreground: oklch(14% 0.025 195); - --secondary: oklch(25% 0.04 264.7); - --secondary-foreground: oklch(97.1% 0.01 264.7); + --secondary: oklch(24% 0.03 220); + --secondary-foreground: oklch(96% 0.01 220); - --muted: oklch(25% 0.04 264.7); - --muted-foreground: oklch(71.1% 0.02 264.7); + --muted: oklch(24% 0.025 220); + --muted-foreground: oklch(65% 0.02 220); - --accent: oklch(25% 0.04 264.7); - --accent-foreground: oklch(97.1% 0.01 264.7); + --accent: oklch(22% 0.03 220); + --accent-foreground: oklch(96% 0.01 220); - --destructive: oklch(55% 0.2 27.3); - --destructive-foreground: oklch(97.1% 0.01 264.7); + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(96% 0.01 25); - --border: oklch(28% 0.04 264.7); - --input: oklch(28% 0.04 264.7); - --ring: oklch(56% 0.2 259); + --border: oklch(26% 0.025 220); + --input: oklch(26% 0.025 220); + --ring: oklch(65% 0.14 195); - --sidebar-background: oklch(12% 0.035 264.7); - --sidebar-foreground: oklch(75% 0.02 264.7); - --sidebar-primary: oklch(62.8% 0.19 259); - --sidebar-primary-foreground: oklch(14% 0.02 264.7); - --sidebar-accent: oklch(22% 0.035 264.7); - --sidebar-accent-foreground: oklch(97.1% 0.01 264.7); - --sidebar-border: oklch(25% 0.04 264.7); - --sidebar-ring: oklch(56% 0.2 259); + --sidebar-background: oklch(12% 0.025 220); + --sidebar-foreground: oklch(70% 0.02 220); + --sidebar-primary: oklch(65% 0.14 195); + --sidebar-primary-foreground: oklch(14% 0.025 195); + --sidebar-accent: oklch(20% 0.025 220); + --sidebar-accent-foreground: oklch(96% 0.01 220); + --sidebar-border: oklch(24% 0.025 220); + --sidebar-ring: oklch(65% 0.14 195); + + --gradient-primary: linear-gradient(135deg, oklch(65% 0.14 195), oklch(58% 0.12 210)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(22% 0.03 195 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.06) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.3); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.4), 0 2px 4px -2px oklch(0% 0 0 / 0.3); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.5), 0 4px 6px -4px oklch(0% 0 0 / 0.3); + --shadow-primary: 0 4px 14px -3px oklch(65% 0.14 195 / 0.4); + + --chart-1: oklch(65% 0.14 195); + --chart-2: oklch(70% 0.14 25); + --chart-3: oklch(65% 0.12 280); + --chart-4: oklch(72% 0.14 140); + --chart-5: oklch(68% 0.16 45); } -/* Medical Professional Theme - Light (IBM Plex Sans) */ -[data-theme='medical-light'] { - --font-family: var(--font-ibm-plex); +/* ============================================ + MEADOW - Natural & Organic + Fresh green with gold accents, earthy and calming + Font: DM Sans | Radius: Softer | Borders: Subtle + ============================================ */ - --background: oklch(99.5% 0.005 220); - --foreground: oklch(20% 0.02 220); +[data-theme='meadow-light'] { + --font-family: var(--font-source); + --radius-base: 0.625rem; + --border-width: 1px; - --card: oklch(100% 0 0); - --card-foreground: oklch(20% 0.02 220); + --background: oklch(98% 0.012 145); + --foreground: oklch(20% 0.03 145); - --popover: oklch(100% 0 0); - --popover-foreground: oklch(20% 0.02 220); + --card: oklch(99.5% 0.008 145); + --card-foreground: oklch(20% 0.03 145); - --primary: oklch(48% 0.15 220); - --primary-foreground: oklch(98% 0.01 220); + --popover: oklch(99.5% 0.008 145); + --popover-foreground: oklch(20% 0.03 145); - --secondary: oklch(96% 0.015 220); - --secondary-foreground: oklch(25% 0.02 220); + --primary: oklch(50% 0.13 150); + --primary-foreground: oklch(98% 0.01 150); + + --secondary: oklch(95% 0.02 145); + --secondary-foreground: oklch(25% 0.025 145); - --muted: oklch(96% 0.015 220); - --muted-foreground: oklch(50% 0.02 220); + --muted: oklch(95% 0.018 145); + --muted-foreground: oklch(45% 0.02 145); - --accent: oklch(92% 0.03 180); - --accent-foreground: oklch(25% 0.02 180); + --accent: oklch(94% 0.02 145); + --accent-foreground: oklch(25% 0.025 145); --destructive: oklch(55% 0.22 25); --destructive-foreground: oklch(98% 0.01 25); - --border: oklch(90% 0.015 220); - --input: oklch(90% 0.015 220); - --ring: oklch(48% 0.15 220); + --border: oklch(90% 0.02 145); + --input: oklch(90% 0.02 145); + --ring: oklch(50% 0.13 150); + + --sidebar-background: oklch(96.5% 0.015 145); + --sidebar-foreground: oklch(35% 0.025 145); + --sidebar-primary: oklch(50% 0.13 150); + --sidebar-primary-foreground: oklch(98% 0.01 150); + --sidebar-accent: oklch(93% 0.02 145); + --sidebar-accent-foreground: oklch(25% 0.025 145); + --sidebar-border: oklch(90% 0.02 145); + --sidebar-ring: oklch(50% 0.13 150); + + --gradient-primary: linear-gradient(135deg, oklch(50% 0.13 150), oklch(52% 0.11 140)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(92% 0.03 145 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(25% 0.03 145 / 0.06); + --shadow-md: 0 4px 6px -1px oklch(25% 0.03 145 / 0.08), 0 2px 4px -2px oklch(25% 0.03 145 / 0.05); + --shadow-lg: 0 10px 15px -3px oklch(25% 0.03 145 / 0.1), 0 4px 6px -4px oklch(25% 0.03 145 / 0.06); + --shadow-primary: 0 4px 14px -3px oklch(50% 0.13 150 / 0.35); + + --chart-1: oklch(50% 0.13 150); + --chart-2: oklch(72% 0.14 85); + --chart-3: oklch(55% 0.1 200); + --chart-4: oklch(60% 0.15 320); + --chart-5: oklch(65% 0.14 35); +} - --sidebar-background: oklch(97% 0.01 220); - --sidebar-foreground: oklch(35% 0.02 220); - --sidebar-primary: oklch(48% 0.15 220); - --sidebar-primary-foreground: oklch(98% 0.01 220); - --sidebar-accent: oklch(94% 0.02 220); - --sidebar-accent-foreground: oklch(25% 0.02 220); - --sidebar-border: oklch(90% 0.015 220); - --sidebar-ring: oklch(48% 0.15 220); +[data-theme='meadow-dark'] { + --font-family: var(--font-source); + --radius-base: 0.625rem; + --border-width: 1px; + + --background: oklch(13% 0.025 145); + --foreground: oklch(95% 0.01 145); + + --card: oklch(16% 0.025 145); + --card-foreground: oklch(95% 0.01 145); + + --popover: oklch(16% 0.025 145); + --popover-foreground: oklch(95% 0.01 145); + + --primary: oklch(62% 0.12 150); + --primary-foreground: oklch(13% 0.025 150); + + --secondary: oklch(24% 0.03 145); + --secondary-foreground: oklch(95% 0.01 145); + + --muted: oklch(24% 0.025 145); + --muted-foreground: oklch(65% 0.02 145); + + --accent: oklch(21% 0.03 145); + --accent-foreground: oklch(95% 0.01 145); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(95% 0.01 25); + + --border: oklch(26% 0.025 145); + --input: oklch(26% 0.025 145); + --ring: oklch(62% 0.12 150); + + --sidebar-background: oklch(11% 0.025 145); + --sidebar-foreground: oklch(70% 0.02 145); + --sidebar-primary: oklch(62% 0.12 150); + --sidebar-primary-foreground: oklch(13% 0.025 150); + --sidebar-accent: oklch(19% 0.025 145); + --sidebar-accent-foreground: oklch(95% 0.01 145); + --sidebar-border: oklch(24% 0.025 145); + --sidebar-ring: oklch(62% 0.12 150); + + --gradient-primary: linear-gradient(135deg, oklch(62% 0.12 150), oklch(55% 0.1 140)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(22% 0.03 145 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.06) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.3); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.4), 0 2px 4px -2px oklch(0% 0 0 / 0.3); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.5), 0 4px 6px -4px oklch(0% 0 0 / 0.3); + --shadow-primary: 0 4px 14px -3px oklch(62% 0.12 150 / 0.4); + + --chart-1: oklch(62% 0.12 150); + --chart-2: oklch(75% 0.13 85); + --chart-3: oklch(60% 0.1 200); + --chart-4: oklch(65% 0.14 320); + --chart-5: oklch(68% 0.14 35); } -/* Medical Professional Theme - Dark (IBM Plex Sans) */ -[data-theme='medical-dark'] { - --font-family: var(--font-ibm-plex); +/* ============================================ + BLOOM - Soft & Gentle Pastel + Soft rose/lavender, airy and feminine + Font: Plus Jakarta Sans | Radius: Rounded | Borders: Soft + ============================================ */ - --background: oklch(15% 0.03 220); - --foreground: oklch(96% 0.01 220); +[data-theme='bloom-light'] { + --font-family: var(--font-inter); + --radius-base: 0.75rem; + --border-width: 1px; - --card: oklch(18% 0.03 220); - --card-foreground: oklch(96% 0.01 220); + --background: oklch(98% 0.015 350); + --foreground: oklch(25% 0.03 350); - --popover: oklch(18% 0.03 220); - --popover-foreground: oklch(96% 0.01 220); + --card: oklch(99.5% 0.01 350); + --card-foreground: oklch(25% 0.03 350); - --primary: oklch(60% 0.15 200); - --primary-foreground: oklch(15% 0.02 200); + --popover: oklch(99.5% 0.01 350); + --popover-foreground: oklch(25% 0.03 350); - --secondary: oklch(25% 0.035 220); - --secondary-foreground: oklch(96% 0.01 220); + --primary: oklch(60% 0.15 350); + --primary-foreground: oklch(98% 0.01 350); - --muted: oklch(25% 0.035 220); - --muted-foreground: oklch(70% 0.02 220); + --secondary: oklch(95% 0.02 350); + --secondary-foreground: oklch(30% 0.025 350); - --accent: oklch(30% 0.05 180); - --accent-foreground: oklch(96% 0.01 180); + --muted: oklch(95% 0.018 350); + --muted-foreground: oklch(45% 0.02 350); - --destructive: oklch(50% 0.18 25); - --destructive-foreground: oklch(96% 0.01 25); + --accent: oklch(94% 0.02 350); + --accent-foreground: oklch(30% 0.025 350); - --border: oklch(28% 0.035 220); - --input: oklch(28% 0.035 220); - --ring: oklch(60% 0.15 200); + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); - --sidebar-background: oklch(12% 0.03 220); - --sidebar-foreground: oklch(75% 0.02 220); - --sidebar-primary: oklch(60% 0.15 200); - --sidebar-primary-foreground: oklch(15% 0.02 200); - --sidebar-accent: oklch(22% 0.03 220); - --sidebar-accent-foreground: oklch(96% 0.01 220); - --sidebar-border: oklch(25% 0.035 220); - --sidebar-ring: oklch(60% 0.15 200); + --border: oklch(91% 0.02 350); + --input: oklch(91% 0.02 350); + --ring: oklch(60% 0.15 350); + + --sidebar-background: oklch(97% 0.018 350); + --sidebar-foreground: oklch(35% 0.025 350); + --sidebar-primary: oklch(60% 0.15 350); + --sidebar-primary-foreground: oklch(98% 0.01 350); + --sidebar-accent: oklch(94% 0.02 350); + --sidebar-accent-foreground: oklch(30% 0.025 350); + --sidebar-border: oklch(91% 0.02 350); + --sidebar-ring: oklch(60% 0.15 350); + + --gradient-primary: linear-gradient(135deg, oklch(60% 0.15 350), oklch(62% 0.13 320)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(92% 0.03 350 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(30% 0.03 350 / 0.06); + --shadow-md: 0 4px 6px -1px oklch(30% 0.03 350 / 0.08), 0 2px 4px -2px oklch(30% 0.03 350 / 0.05); + --shadow-lg: 0 10px 15px -3px oklch(30% 0.03 350 / 0.1), 0 4px 6px -4px oklch(30% 0.03 350 / 0.06); + --shadow-primary: 0 4px 14px -3px oklch(60% 0.15 350 / 0.35); + + --chart-1: oklch(60% 0.15 350); + --chart-2: oklch(70% 0.12 280); + --chart-3: oklch(55% 0.1 180); + --chart-4: oklch(65% 0.14 60); + --chart-5: oklch(62% 0.15 320); +} + +[data-theme='bloom-dark'] { + --font-family: var(--font-inter); + --radius-base: 0.75rem; + --border-width: 1px; + + --background: oklch(14% 0.025 350); + --foreground: oklch(95% 0.012 350); + + --card: oklch(17% 0.025 350); + --card-foreground: oklch(95% 0.012 350); + + --popover: oklch(17% 0.025 350); + --popover-foreground: oklch(95% 0.012 350); + + --primary: oklch(70% 0.13 350); + --primary-foreground: oklch(14% 0.025 350); + + --secondary: oklch(25% 0.03 350); + --secondary-foreground: oklch(95% 0.012 350); + + --muted: oklch(25% 0.025 350); + --muted-foreground: oklch(65% 0.02 350); + + --accent: oklch(22% 0.03 350); + --accent-foreground: oklch(95% 0.012 350); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(95% 0.01 25); + + --border: oklch(27% 0.025 350); + --input: oklch(27% 0.025 350); + --ring: oklch(70% 0.13 350); + + --sidebar-background: oklch(12% 0.025 350); + --sidebar-foreground: oklch(70% 0.02 350); + --sidebar-primary: oklch(70% 0.13 350); + --sidebar-primary-foreground: oklch(14% 0.025 350); + --sidebar-accent: oklch(20% 0.025 350); + --sidebar-accent-foreground: oklch(95% 0.012 350); + --sidebar-border: oklch(25% 0.025 350); + --sidebar-ring: oklch(70% 0.13 350); + + --gradient-primary: linear-gradient(135deg, oklch(70% 0.13 350), oklch(65% 0.11 320)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(22% 0.03 350 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.06) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.3); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.4), 0 2px 4px -2px oklch(0% 0 0 / 0.3); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.5), 0 4px 6px -4px oklch(0% 0 0 / 0.3); + --shadow-primary: 0 4px 14px -3px oklch(70% 0.13 350 / 0.4); + + --chart-1: oklch(70% 0.13 350); + --chart-2: oklch(72% 0.12 280); + --chart-3: oklch(60% 0.1 180); + --chart-4: oklch(68% 0.14 60); + --chart-5: oklch(65% 0.15 320); } -/* Classic Theme - Light (Source Serif Pro) */ -[data-theme='classic-light'] { - --font-family: var(--font-source-serif); +/* ============================================ + LATTE - Warm & Cozy + Coffee browns with cream accents, comfortable and inviting + Font: DM Sans | Radius: Rounded | Borders: Warm + ============================================ */ + +[data-theme='latte-light'] { + --font-family: var(--font-source); + --radius-base: 0.625rem; + --border-width: 1px; - --background: oklch(98% 0.01 80); - --foreground: oklch(22% 0.02 60); + --background: oklch(97% 0.015 70); + --foreground: oklch(25% 0.04 50); - --card: oklch(99% 0.008 80); - --card-foreground: oklch(22% 0.02 60); + --card: oklch(99% 0.012 70); + --card-foreground: oklch(25% 0.04 50); - --popover: oklch(99% 0.008 80); - --popover-foreground: oklch(22% 0.02 60); + --popover: oklch(99% 0.012 70); + --popover-foreground: oklch(25% 0.04 50); - --primary: oklch(45% 0.1 40); - --primary-foreground: oklch(97% 0.01 40); + --primary: oklch(45% 0.1 55); + --primary-foreground: oklch(97% 0.015 55); - --secondary: oklch(95% 0.015 80); - --secondary-foreground: oklch(28% 0.02 60); + --secondary: oklch(94% 0.02 70); + --secondary-foreground: oklch(30% 0.035 50); - --muted: oklch(95% 0.015 80); - --muted-foreground: oklch(50% 0.02 60); + --muted: oklch(94% 0.018 70); + --muted-foreground: oklch(48% 0.03 50); - --accent: oklch(93% 0.02 80); - --accent-foreground: oklch(28% 0.02 60); + --accent: oklch(93% 0.02 70); + --accent-foreground: oklch(30% 0.035 50); - --destructive: oklch(52% 0.2 25); + --destructive: oklch(55% 0.22 25); --destructive-foreground: oklch(97% 0.01 25); - --border: oklch(90% 0.015 80); - --input: oklch(90% 0.015 80); - --ring: oklch(45% 0.1 40); - - --sidebar-background: oklch(96% 0.012 80); - --sidebar-foreground: oklch(35% 0.02 60); - --sidebar-primary: oklch(45% 0.1 40); - --sidebar-primary-foreground: oklch(97% 0.01 40); - --sidebar-accent: oklch(93% 0.018 80); - --sidebar-accent-foreground: oklch(28% 0.02 60); - --sidebar-border: oklch(88% 0.015 80); - --sidebar-ring: oklch(45% 0.1 40); + --border: oklch(89% 0.02 70); + --input: oklch(89% 0.02 70); + --ring: oklch(45% 0.1 55); + + --sidebar-background: oklch(95% 0.018 70); + --sidebar-foreground: oklch(38% 0.035 50); + --sidebar-primary: oklch(45% 0.1 55); + --sidebar-primary-foreground: oklch(97% 0.015 55); + --sidebar-accent: oklch(92% 0.02 70); + --sidebar-accent-foreground: oklch(30% 0.035 50); + --sidebar-border: oklch(89% 0.02 70); + --sidebar-ring: oklch(45% 0.1 55); + + --gradient-primary: linear-gradient(135deg, oklch(45% 0.1 55), oklch(50% 0.08 40)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(90% 0.03 70 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.4) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(30% 0.04 50 / 0.08); + --shadow-md: 0 4px 6px -1px oklch(30% 0.04 50 / 0.1), 0 2px 4px -2px oklch(30% 0.04 50 / 0.06); + --shadow-lg: 0 10px 15px -3px oklch(30% 0.04 50 / 0.12), 0 4px 6px -4px oklch(30% 0.04 50 / 0.07); + --shadow-primary: 0 4px 14px -3px oklch(45% 0.1 55 / 0.35); + + --chart-1: oklch(45% 0.1 55); + --chart-2: oklch(70% 0.12 80); + --chart-3: oklch(55% 0.08 180); + --chart-4: oklch(62% 0.14 25); + --chart-5: oklch(58% 0.12 140); } -/* Classic Theme - Dark (Source Serif Pro) */ -[data-theme='classic-dark'] { - --font-family: var(--font-source-serif); +[data-theme='latte-dark'] { + --font-family: var(--font-source); + --radius-base: 0.625rem; + --border-width: 1px; - --background: oklch(18% 0.02 60); - --foreground: oklch(94% 0.01 80); + --background: oklch(15% 0.025 50); + --foreground: oklch(94% 0.015 70); - --card: oklch(21% 0.02 60); - --card-foreground: oklch(94% 0.01 80); + --card: oklch(18% 0.025 50); + --card-foreground: oklch(94% 0.015 70); - --popover: oklch(21% 0.02 60); - --popover-foreground: oklch(94% 0.01 80); + --popover: oklch(18% 0.025 50); + --popover-foreground: oklch(94% 0.015 70); - --primary: oklch(65% 0.12 45); - --primary-foreground: oklch(18% 0.02 45); + --primary: oklch(70% 0.1 55); + --primary-foreground: oklch(15% 0.025 55); - --secondary: oklch(28% 0.025 60); - --secondary-foreground: oklch(94% 0.01 80); + --secondary: oklch(25% 0.03 50); + --secondary-foreground: oklch(94% 0.015 70); - --muted: oklch(28% 0.025 60); - --muted-foreground: oklch(68% 0.02 80); + --muted: oklch(25% 0.025 50); + --muted-foreground: oklch(65% 0.02 70); - --accent: oklch(32% 0.03 60); - --accent-foreground: oklch(94% 0.01 80); + --accent: oklch(22% 0.03 50); + --accent-foreground: oklch(94% 0.015 70); - --destructive: oklch(48% 0.17 25); + --destructive: oklch(55% 0.2 25); --destructive-foreground: oklch(94% 0.01 25); - --border: oklch(30% 0.025 60); - --input: oklch(30% 0.025 60); - --ring: oklch(65% 0.12 45); - - --sidebar-background: oklch(15% 0.02 60); - --sidebar-foreground: oklch(72% 0.02 80); - --sidebar-primary: oklch(65% 0.12 45); - --sidebar-primary-foreground: oklch(18% 0.02 45); - --sidebar-accent: oklch(25% 0.025 60); - --sidebar-accent-foreground: oklch(94% 0.01 80); - --sidebar-border: oklch(28% 0.025 60); - --sidebar-ring: oklch(65% 0.12 45); + --border: oklch(27% 0.025 50); + --input: oklch(27% 0.025 50); + --ring: oklch(70% 0.1 55); + + --sidebar-background: oklch(13% 0.025 50); + --sidebar-foreground: oklch(70% 0.02 70); + --sidebar-primary: oklch(70% 0.1 55); + --sidebar-primary-foreground: oklch(15% 0.025 55); + --sidebar-accent: oklch(20% 0.025 50); + --sidebar-accent-foreground: oklch(94% 0.015 70); + --sidebar-border: oklch(25% 0.025 50); + --sidebar-ring: oklch(70% 0.1 55); + + --gradient-primary: linear-gradient(135deg, oklch(70% 0.1 55), oklch(65% 0.08 40)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(22% 0.03 50 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.06) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.3); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.4), 0 2px 4px -2px oklch(0% 0 0 / 0.3); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.5), 0 4px 6px -4px oklch(0% 0 0 / 0.3); + --shadow-primary: 0 4px 14px -3px oklch(70% 0.1 55 / 0.4); + + --chart-1: oklch(70% 0.1 55); + --chart-2: oklch(75% 0.12 80); + --chart-3: oklch(60% 0.08 180); + --chart-4: oklch(68% 0.14 25); + --chart-5: oklch(62% 0.12 140); } -/* Minimal Theme - Light (Space Grotesk) */ -[data-theme='minimal-light'] { - --font-family: var(--font-space-grotesk); +/* ============================================ + SLATE - Sharp & Professional + Clean grays with blue accents, high contrast and modern + Font: Outfit | Radius: Crisp | Borders: Defined + ============================================ */ + +[data-theme='slate-light'] { + --font-family: var(--font-nunito); + --radius-base: 0.375rem; + --border-width: 1px; - --background: oklch(100% 0 0); - --foreground: oklch(15% 0 0); + --background: oklch(99% 0.003 260); + --foreground: oklch(18% 0.01 260); --card: oklch(100% 0 0); - --card-foreground: oklch(15% 0 0); + --card-foreground: oklch(18% 0.01 260); --popover: oklch(100% 0 0); - --popover-foreground: oklch(15% 0 0); + --popover-foreground: oklch(18% 0.01 260); - --primary: oklch(25% 0 0); - --primary-foreground: oklch(100% 0 0); + --primary: oklch(35% 0.02 260); + --primary-foreground: oklch(98% 0.005 260); - --secondary: oklch(96% 0 0); - --secondary-foreground: oklch(20% 0 0); + --secondary: oklch(95% 0.008 260); + --secondary-foreground: oklch(25% 0.01 260); - --muted: oklch(96% 0 0); - --muted-foreground: oklch(50% 0 0); + --muted: oklch(95% 0.005 260); + --muted-foreground: oklch(48% 0.01 260); - --accent: oklch(96% 0 0); - --accent-foreground: oklch(20% 0 0); + --accent: oklch(94% 0.008 260); + --accent-foreground: oklch(25% 0.01 260); --destructive: oklch(55% 0.22 25); - --destructive-foreground: oklch(100% 0 0); - - --border: oklch(88% 0 0); - --input: oklch(88% 0 0); - --ring: oklch(25% 0 0); - - --sidebar-background: oklch(98% 0 0); - --sidebar-foreground: oklch(35% 0 0); - --sidebar-primary: oklch(25% 0 0); - --sidebar-primary-foreground: oklch(100% 0 0); - --sidebar-accent: oklch(94% 0 0); - --sidebar-accent-foreground: oklch(20% 0 0); - --sidebar-border: oklch(88% 0 0); - --sidebar-ring: oklch(25% 0 0); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(90% 0.008 260); + --input: oklch(90% 0.008 260); + --ring: oklch(35% 0.02 260); + + --sidebar-background: oklch(97.5% 0.005 260); + --sidebar-foreground: oklch(35% 0.01 260); + --sidebar-primary: oklch(35% 0.02 260); + --sidebar-primary-foreground: oklch(98% 0.005 260); + --sidebar-accent: oklch(94% 0.008 260); + --sidebar-accent-foreground: oklch(25% 0.01 260); + --sidebar-border: oklch(90% 0.008 260); + --sidebar-ring: oklch(35% 0.02 260); + + --gradient-primary: linear-gradient(135deg, oklch(35% 0.02 260), oklch(40% 0.025 250)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(94% 0.01 260 / 0.3), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(18% 0.01 260 / 0.05); + --shadow-md: 0 4px 6px -1px oklch(18% 0.01 260 / 0.07), 0 2px 4px -2px oklch(18% 0.01 260 / 0.04); + --shadow-lg: 0 10px 15px -3px oklch(18% 0.01 260 / 0.08), 0 4px 6px -4px oklch(18% 0.01 260 / 0.05); + --shadow-primary: 0 4px 14px -3px oklch(35% 0.02 260 / 0.3); + + --chart-1: oklch(35% 0.02 260); + --chart-2: oklch(58% 0.14 245); + --chart-3: oklch(55% 0.12 160); + --chart-4: oklch(60% 0.14 320); + --chart-5: oklch(65% 0.15 45); } -/* Minimal Theme - Dark (Space Grotesk) */ -[data-theme='minimal-dark'] { - --font-family: var(--font-space-grotesk); +[data-theme='slate-dark'] { + --font-family: var(--font-nunito); + --radius-base: 0.375rem; + --border-width: 1px; + + --background: oklch(12% 0.008 260); + --foreground: oklch(96% 0.005 260); + + --card: oklch(15% 0.008 260); + --card-foreground: oklch(96% 0.005 260); + + --popover: oklch(15% 0.008 260); + --popover-foreground: oklch(96% 0.005 260); + + --primary: oklch(96% 0.005 260); + --primary-foreground: oklch(12% 0.008 260); - --background: oklch(10% 0 0); - --foreground: oklch(98% 0 0); + --secondary: oklch(22% 0.01 260); + --secondary-foreground: oklch(96% 0.005 260); - --card: oklch(13% 0 0); - --card-foreground: oklch(98% 0 0); + --muted: oklch(22% 0.008 260); + --muted-foreground: oklch(62% 0.01 260); - --popover: oklch(13% 0 0); - --popover-foreground: oklch(98% 0 0); + --accent: oklch(20% 0.01 260); + --accent-foreground: oklch(96% 0.005 260); - --primary: oklch(98% 0 0); - --primary-foreground: oklch(10% 0 0); + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(96% 0.005 25); - --secondary: oklch(20% 0 0); - --secondary-foreground: oklch(98% 0 0); + --border: oklch(25% 0.01 260); + --input: oklch(25% 0.01 260); + --ring: oklch(80% 0.008 260); - --muted: oklch(20% 0 0); - --muted-foreground: oklch(65% 0 0); + --sidebar-background: oklch(10% 0.008 260); + --sidebar-foreground: oklch(68% 0.008 260); + --sidebar-primary: oklch(96% 0.005 260); + --sidebar-primary-foreground: oklch(12% 0.008 260); + --sidebar-accent: oklch(18% 0.008 260); + --sidebar-accent-foreground: oklch(96% 0.005 260); + --sidebar-border: oklch(23% 0.01 260); + --sidebar-ring: oklch(80% 0.008 260); - --accent: oklch(20% 0 0); - --accent-foreground: oklch(98% 0 0); + --gradient-primary: linear-gradient(135deg, oklch(96% 0.005 260), oklch(88% 0.01 250)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(20% 0.012 260 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.04) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.35); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.45), 0 2px 4px -2px oklch(0% 0 0 / 0.35); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.55), 0 4px 6px -4px oklch(0% 0 0 / 0.35); + --shadow-primary: 0 4px 14px -3px oklch(96% 0.005 260 / 0.15); + + --chart-1: oklch(96% 0.005 260); + --chart-2: oklch(62% 0.14 245); + --chart-3: oklch(60% 0.12 160); + --chart-4: oklch(65% 0.14 320); + --chart-5: oklch(68% 0.15 45); +} + +/* ============================================ + MIDNIGHT - Elegant Purple/Indigo + Soft lavender light, deep purple dark + Font: Outfit | Radius: Default | Borders: Elegant + ============================================ */ + +[data-theme='midnight-light'] { + --font-family: var(--font-nunito); + --radius-base: 0.5rem; + --border-width: 1px; + + --background: oklch(97% 0.015 280); + --foreground: oklch(20% 0.03 280); + + --card: oklch(99% 0.01 280); + --card-foreground: oklch(20% 0.03 280); + + --popover: oklch(99% 0.01 280); + --popover-foreground: oklch(20% 0.03 280); + + --primary: oklch(55% 0.18 280); + --primary-foreground: oklch(98% 0.01 280); + + --secondary: oklch(94% 0.02 280); + --secondary-foreground: oklch(25% 0.025 280); + + --muted: oklch(94% 0.018 280); + --muted-foreground: oklch(45% 0.02 280); + + --accent: oklch(93% 0.02 280); + --accent-foreground: oklch(25% 0.025 280); --destructive: oklch(55% 0.22 25); - --destructive-foreground: oklch(98% 0 0); - - --border: oklch(25% 0 0); - --input: oklch(25% 0 0); - --ring: oklch(85% 0 0); - - --sidebar-background: oklch(8% 0 0); - --sidebar-foreground: oklch(70% 0 0); - --sidebar-primary: oklch(98% 0 0); - --sidebar-primary-foreground: oklch(10% 0 0); - --sidebar-accent: oklch(18% 0 0); - --sidebar-accent-foreground: oklch(98% 0 0); - --sidebar-border: oklch(22% 0 0); - --sidebar-ring: oklch(85% 0 0); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(90% 0.02 280); + --input: oklch(90% 0.02 280); + --ring: oklch(55% 0.18 280); + + --sidebar-background: oklch(96% 0.018 280); + --sidebar-foreground: oklch(35% 0.025 280); + --sidebar-primary: oklch(55% 0.18 280); + --sidebar-primary-foreground: oklch(98% 0.01 280); + --sidebar-accent: oklch(92% 0.02 280); + --sidebar-accent-foreground: oklch(25% 0.025 280); + --sidebar-border: oklch(90% 0.02 280); + --sidebar-ring: oklch(55% 0.18 280); + + --gradient-primary: linear-gradient(135deg, oklch(55% 0.18 280), oklch(58% 0.15 300)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(92% 0.03 280 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(25% 0.03 280 / 0.06); + --shadow-md: 0 4px 6px -1px oklch(25% 0.03 280 / 0.08), 0 2px 4px -2px oklch(25% 0.03 280 / 0.05); + --shadow-lg: 0 10px 15px -3px oklch(25% 0.03 280 / 0.1), 0 4px 6px -4px oklch(25% 0.03 280 / 0.06); + --shadow-primary: 0 4px 14px -3px oklch(55% 0.18 280 / 0.35); + + --chart-1: oklch(55% 0.18 280); + --chart-2: oklch(60% 0.14 320); + --chart-3: oklch(55% 0.12 200); + --chart-4: oklch(65% 0.15 45); + --chart-5: oklch(58% 0.14 160); } -/* Apply font family globally when theme is set */ +[data-theme='midnight-dark'] { + --font-family: var(--font-nunito); + --radius-base: 0.5rem; + --border-width: 1px; + + --background: oklch(5% 0.015 280); + --foreground: oklch(95% 0.01 280); + + --card: oklch(10% 0.02 280); + --card-foreground: oklch(95% 0.01 280); + + --popover: oklch(10% 0.02 280); + --popover-foreground: oklch(95% 0.01 280); + + --primary: oklch(70% 0.15 280); + --primary-foreground: oklch(5% 0.015 280); + + --secondary: oklch(18% 0.02 280); + --secondary-foreground: oklch(95% 0.01 280); + + --muted: oklch(18% 0.015 280); + --muted-foreground: oklch(60% 0.015 280); + + --accent: oklch(15% 0.02 280); + --accent-foreground: oklch(95% 0.01 280); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(95% 0.01 25); + + --border: oklch(20% 0.02 280); + --input: oklch(20% 0.02 280); + --ring: oklch(70% 0.15 280); + + --sidebar-background: oklch(3% 0.015 280); + --sidebar-foreground: oklch(65% 0.015 280); + --sidebar-primary: oklch(70% 0.15 280); + --sidebar-primary-foreground: oklch(5% 0.015 280); + --sidebar-accent: oklch(12% 0.02 280); + --sidebar-accent-foreground: oklch(95% 0.01 280); + --sidebar-border: oklch(18% 0.02 280); + --sidebar-ring: oklch(70% 0.15 280); + + --gradient-primary: linear-gradient(135deg, oklch(70% 0.15 280), oklch(65% 0.12 300)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(15% 0.03 280 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.03) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.5); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.6), 0 2px 4px -2px oklch(0% 0 0 / 0.5); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.7), 0 4px 6px -4px oklch(0% 0 0 / 0.5); + --shadow-primary: 0 4px 14px -3px oklch(70% 0.15 280 / 0.5); + + --chart-1: oklch(70% 0.15 280); + --chart-2: oklch(65% 0.14 200); + --chart-3: oklch(68% 0.12 340); + --chart-4: oklch(62% 0.15 160); + --chart-5: oklch(72% 0.13 45); +} + +/* ============================================ + OBSIDIAN - True Black AMOLED + Pure black backgrounds for OLED displays + Font: Outfit | Radius: Sharp | Borders: Minimal + ============================================ */ + +[data-theme='obsidian-light'] { + --font-family: var(--font-nunito); + --radius-base: 0.375rem; + --border-width: 1px; + + --background: oklch(99% 0.002 0); + --foreground: oklch(10% 0.005 0); + + --card: oklch(100% 0 0); + --card-foreground: oklch(10% 0.005 0); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(10% 0.005 0); + + --primary: oklch(25% 0.005 0); + --primary-foreground: oklch(98% 0.002 0); + + --secondary: oklch(96% 0.003 0); + --secondary-foreground: oklch(15% 0.005 0); + + --muted: oklch(96% 0.002 0); + --muted-foreground: oklch(45% 0.005 0); + + --accent: oklch(95% 0.003 0); + --accent-foreground: oklch(15% 0.005 0); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(92% 0.003 0); + --input: oklch(92% 0.003 0); + --ring: oklch(25% 0.005 0); + + --sidebar-background: oklch(98% 0.002 0); + --sidebar-foreground: oklch(30% 0.005 0); + --sidebar-primary: oklch(25% 0.005 0); + --sidebar-primary-foreground: oklch(98% 0.002 0); + --sidebar-accent: oklch(95% 0.003 0); + --sidebar-accent-foreground: oklch(15% 0.005 0); + --sidebar-border: oklch(92% 0.003 0); + --sidebar-ring: oklch(25% 0.005 0); + + --gradient-primary: linear-gradient(135deg, oklch(25% 0.005 0), oklch(35% 0.005 0)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(95% 0.005 0 / 0.3), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(10% 0.005 0 / 0.06); + --shadow-md: 0 4px 6px -1px oklch(10% 0.005 0 / 0.08), 0 2px 4px -2px oklch(10% 0.005 0 / 0.05); + --shadow-lg: 0 10px 15px -3px oklch(10% 0.005 0 / 0.1), 0 4px 6px -4px oklch(10% 0.005 0 / 0.06); + --shadow-primary: 0 4px 14px -3px oklch(25% 0.005 0 / 0.3); + + --chart-1: oklch(25% 0.005 0); + --chart-2: oklch(55% 0.12 220); + --chart-3: oklch(50% 0.1 150); + --chart-4: oklch(55% 0.14 25); + --chart-5: oklch(50% 0.12 280); +} + +[data-theme='obsidian-dark'] { + --font-family: var(--font-nunito); + --radius-base: 0.375rem; + --border-width: 1px; + + /* True black for AMOLED */ + --background: oklch(0% 0 0); + --foreground: oklch(95% 0.005 0); + + --card: oklch(8% 0.005 0); + --card-foreground: oklch(95% 0.005 0); + + --popover: oklch(8% 0.005 0); + --popover-foreground: oklch(95% 0.005 0); + + --primary: oklch(95% 0.005 0); + --primary-foreground: oklch(5% 0.005 0); + + --secondary: oklch(15% 0.005 0); + --secondary-foreground: oklch(95% 0.005 0); + + --muted: oklch(15% 0.005 0); + --muted-foreground: oklch(60% 0.005 0); + + --accent: oklch(12% 0.005 0); + --accent-foreground: oklch(95% 0.005 0); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(95% 0.005 25); + + --border: oklch(20% 0.005 0); + --input: oklch(20% 0.005 0); + --ring: oklch(80% 0.005 0); + + --sidebar-background: oklch(0% 0 0); + --sidebar-foreground: oklch(65% 0.005 0); + --sidebar-primary: oklch(95% 0.005 0); + --sidebar-primary-foreground: oklch(5% 0.005 0); + --sidebar-accent: oklch(10% 0.005 0); + --sidebar-accent-foreground: oklch(95% 0.005 0); + --sidebar-border: oklch(18% 0.005 0); + --sidebar-ring: oklch(80% 0.005 0); + + --gradient-primary: linear-gradient(135deg, oklch(95% 0.005 0), oklch(85% 0.005 0)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(10% 0.01 0 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.03) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.6); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.7), 0 2px 4px -2px oklch(0% 0 0 / 0.5); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.8), 0 4px 6px -4px oklch(0% 0 0 / 0.6); + --shadow-primary: 0 4px 14px -3px oklch(95% 0.005 0 / 0.15); + + --chart-1: oklch(95% 0.005 0); + --chart-2: oklch(60% 0.12 220); + --chart-3: oklch(55% 0.1 150); + --chart-4: oklch(60% 0.14 25); + --chart-5: oklch(55% 0.12 280); +} + +/* ============================================ + OCEAN - Deep Blue Depths + Calming blue tones, professional and trustworthy + Font: Plus Jakarta Sans | Radius: Medium | Borders: Soft + ============================================ */ + +[data-theme='ocean-light'] { + --font-family: var(--font-inter); + --radius-base: 0.5rem; + --border-width: 1px; + + --background: oklch(98% 0.01 230); + --foreground: oklch(18% 0.025 230); + + --card: oklch(100% 0 0); + --card-foreground: oklch(18% 0.025 230); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(18% 0.025 230); + + --primary: oklch(50% 0.15 240); + --primary-foreground: oklch(98% 0.01 240); + + --secondary: oklch(95% 0.015 230); + --secondary-foreground: oklch(25% 0.02 230); + + --muted: oklch(95% 0.012 230); + --muted-foreground: oklch(45% 0.02 230); + + --accent: oklch(94% 0.015 230); + --accent-foreground: oklch(25% 0.02 230); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(90% 0.012 230); + --input: oklch(90% 0.012 230); + --ring: oklch(50% 0.15 240); + + --sidebar-background: oklch(97% 0.012 230); + --sidebar-foreground: oklch(35% 0.02 230); + --sidebar-primary: oklch(50% 0.15 240); + --sidebar-primary-foreground: oklch(98% 0.01 240); + --sidebar-accent: oklch(93% 0.015 230); + --sidebar-accent-foreground: oklch(25% 0.02 230); + --sidebar-border: oklch(90% 0.012 230); + --sidebar-ring: oklch(50% 0.15 240); + + --gradient-primary: linear-gradient(135deg, oklch(50% 0.15 240), oklch(55% 0.12 220)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(92% 0.02 230 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(20% 0.02 230 / 0.06); + --shadow-md: 0 4px 6px -1px oklch(20% 0.02 230 / 0.08), 0 2px 4px -2px oklch(20% 0.02 230 / 0.05); + --shadow-lg: 0 10px 15px -3px oklch(20% 0.02 230 / 0.1), 0 4px 6px -4px oklch(20% 0.02 230 / 0.06); + --shadow-primary: 0 4px 14px -3px oklch(50% 0.15 240 / 0.35); + + --chart-1: oklch(50% 0.15 240); + --chart-2: oklch(60% 0.14 200); + --chart-3: oklch(55% 0.12 280); + --chart-4: oklch(65% 0.15 160); + --chart-5: oklch(58% 0.14 45); +} + +[data-theme='ocean-dark'] { + --font-family: var(--font-inter); + --radius-base: 0.5rem; + --border-width: 1px; + + --background: oklch(12% 0.025 230); + --foreground: oklch(96% 0.01 230); + + --card: oklch(15% 0.025 230); + --card-foreground: oklch(96% 0.01 230); + + --popover: oklch(15% 0.025 230); + --popover-foreground: oklch(96% 0.01 230); + + --primary: oklch(65% 0.14 240); + --primary-foreground: oklch(12% 0.025 240); + + --secondary: oklch(22% 0.03 230); + --secondary-foreground: oklch(96% 0.01 230); + + --muted: oklch(22% 0.025 230); + --muted-foreground: oklch(65% 0.02 230); + + --accent: oklch(20% 0.03 230); + --accent-foreground: oklch(96% 0.01 230); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(96% 0.01 25); + + --border: oklch(25% 0.025 230); + --input: oklch(25% 0.025 230); + --ring: oklch(65% 0.14 240); + + --sidebar-background: oklch(10% 0.025 230); + --sidebar-foreground: oklch(70% 0.02 230); + --sidebar-primary: oklch(65% 0.14 240); + --sidebar-primary-foreground: oklch(12% 0.025 240); + --sidebar-accent: oklch(18% 0.025 230); + --sidebar-accent-foreground: oklch(96% 0.01 230); + --sidebar-border: oklch(23% 0.025 230); + --sidebar-ring: oklch(65% 0.14 240); + + --gradient-primary: linear-gradient(135deg, oklch(65% 0.14 240), oklch(58% 0.12 220)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(20% 0.03 230 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.05) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.35); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.45), 0 2px 4px -2px oklch(0% 0 0 / 0.35); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.55), 0 4px 6px -4px oklch(0% 0 0 / 0.35); + --shadow-primary: 0 4px 14px -3px oklch(65% 0.14 240 / 0.4); + + --chart-1: oklch(65% 0.14 240); + --chart-2: oklch(65% 0.14 200); + --chart-3: oklch(60% 0.12 280); + --chart-4: oklch(68% 0.15 160); + --chart-5: oklch(62% 0.14 45); +} + +/* ============================================ + RETRO - CRT Terminal Nostalgia + Amber phosphor on dark screen, vintage hospital terminal feel + Font: VT323 | Radius: Sharp | Borders: Pixelated feel + ============================================ */ + +[data-theme='retro-light'] { + --font-family: var(--font-vt323); + --radius-base: 0.25rem; + --border-width: 2px; + + --background: oklch(95% 0.02 85); + --foreground: oklch(25% 0.04 85); + + --card: oklch(98% 0.015 85); + --card-foreground: oklch(25% 0.04 85); + + --popover: oklch(98% 0.015 85); + --popover-foreground: oklch(25% 0.04 85); + + --primary: oklch(50% 0.14 145); + --primary-foreground: oklch(98% 0.01 145); + + --secondary: oklch(90% 0.03 85); + --secondary-foreground: oklch(30% 0.04 85); + + --muted: oklch(92% 0.02 85); + --muted-foreground: oklch(45% 0.03 85); + + --accent: oklch(88% 0.04 85); + --accent-foreground: oklch(30% 0.04 85); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(82% 0.03 85); + --input: oklch(82% 0.03 85); + --ring: oklch(50% 0.14 145); + + --sidebar-background: oklch(92% 0.025 85); + --sidebar-foreground: oklch(35% 0.04 85); + --sidebar-primary: oklch(50% 0.14 145); + --sidebar-primary-foreground: oklch(98% 0.01 145); + --sidebar-accent: oklch(88% 0.03 85); + --sidebar-accent-foreground: oklch(30% 0.04 85); + --sidebar-border: oklch(82% 0.03 85); + --sidebar-ring: oklch(50% 0.14 145); + + --gradient-primary: linear-gradient(135deg, oklch(50% 0.14 145), oklch(55% 0.12 130)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(88% 0.04 85 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.3) 50%, transparent 75%); + + --shadow-sm: 0 2px 0 oklch(25% 0.04 85 / 0.15); + --shadow-md: 0 4px 0 oklch(25% 0.04 85 / 0.12), 0 2px 0 oklch(25% 0.04 85 / 0.08); + --shadow-lg: 0 6px 0 oklch(25% 0.04 85 / 0.1), 0 3px 0 oklch(25% 0.04 85 / 0.06); + --shadow-primary: 0 4px 0 oklch(50% 0.14 145 / 0.4); + + --chart-1: oklch(50% 0.14 145); + --chart-2: oklch(65% 0.15 85); + --chart-3: oklch(55% 0.1 200); + --chart-4: oklch(60% 0.14 25); + --chart-5: oklch(58% 0.12 300); +} + +[data-theme='retro-dark'] { + --font-family: var(--font-vt323); + --radius-base: 0.25rem; + --border-width: 2px; + + /* Classic amber phosphor CRT colors */ + --background: oklch(8% 0.015 85); + --foreground: oklch(80% 0.14 85); + + --card: oklch(12% 0.02 85); + --card-foreground: oklch(80% 0.14 85); + + --popover: oklch(12% 0.02 85); + --popover-foreground: oklch(80% 0.14 85); + + /* Green phosphor primary */ + --primary: oklch(70% 0.18 145); + --primary-foreground: oklch(8% 0.015 145); + + --secondary: oklch(18% 0.025 85); + --secondary-foreground: oklch(80% 0.14 85); + + --muted: oklch(15% 0.02 85); + --muted-foreground: oklch(55% 0.08 85); + + --accent: oklch(16% 0.025 85); + --accent-foreground: oklch(80% 0.14 85); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(95% 0.01 25); + + --border: oklch(25% 0.03 85); + --input: oklch(25% 0.03 85); + --ring: oklch(70% 0.18 145); + + --sidebar-background: oklch(6% 0.015 85); + --sidebar-foreground: oklch(60% 0.1 85); + --sidebar-primary: oklch(70% 0.18 145); + --sidebar-primary-foreground: oklch(8% 0.015 145); + --sidebar-accent: oklch(14% 0.02 85); + --sidebar-accent-foreground: oklch(80% 0.14 85); + --sidebar-border: oklch(22% 0.025 85); + --sidebar-ring: oklch(70% 0.18 145); + + --gradient-primary: linear-gradient(135deg, oklch(70% 0.18 145), oklch(65% 0.15 130)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(15% 0.03 85 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(80% 0.14 85 / 0.05) 50%, transparent 75%); + + /* Blocky, pixelated shadows for retro feel */ + --shadow-sm: 0 2px 0 oklch(0% 0 0 / 0.5); + --shadow-md: 0 4px 0 oklch(0% 0 0 / 0.6), 0 2px 0 oklch(0% 0 0 / 0.4); + --shadow-lg: 0 6px 0 oklch(0% 0 0 / 0.7), 0 3px 0 oklch(0% 0 0 / 0.5); + --shadow-primary: 0 4px 0 oklch(70% 0.18 145 / 0.5); + + --chart-1: oklch(70% 0.18 145); + --chart-2: oklch(75% 0.14 85); + --chart-3: oklch(60% 0.1 200); + --chart-4: oklch(65% 0.14 25); + --chart-5: oklch(62% 0.12 300); +} + +/* Apply font family and radius globally */ [data-theme] body, :root body { font-family: var(--font-family, var(--font-inter)); } + +/* Dynamic border radius based on theme */ +[data-theme] { + --radius-lg: var(--radius-base); + --radius-md: calc(var(--radius-base) - 2px); + --radius-sm: calc(var(--radius-base) - 4px); +} From 17370cb6b494638cc49a90509d0e37362d01af69 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Sun, 18 Jan 2026 23:44:02 +0530 Subject: [PATCH 05/26] feat: redesign patient view with sidebar layout - Add PatientSidebar component with avatar, demographics, contact info, and collapsible remarks - Add PatientSurgeriesCard with search, table, and empty state - Use 12-column grid layout (3:9 split) for better space utilization - Move Edit Patient to ghost button in sidebar header - Move Add Surgery button to surgeries card - Apply consistent card styling with gradients and colored icons --- .../src/routes/patients/view-patient.tsx | 406 +++++++++++++----- 1 file changed, 303 insertions(+), 103 deletions(-) diff --git a/src/renderer/src/routes/patients/view-patient.tsx b/src/renderer/src/routes/patients/view-patient.tsx index b178c9c..852b72c 100644 --- a/src/renderer/src/routes/patients/view-patient.tsx +++ b/src/renderer/src/routes/patients/view-patient.tsx @@ -2,7 +2,25 @@ import { Button } from '@renderer/components/ui/button' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' import { AppLayout } from '@renderer/layouts/AppLayout' import { QueryClient, queryOptions, useQuery, useSuspenseQuery } from '@tanstack/react-query' -import { ChevronDown, EditIcon, MoreHorizontal, PlusSquare } from 'lucide-react' +import { + EditIcon, + MoreHorizontal, + PlusSquare, + Phone, + PhoneCall, + UserPlus, + FileText, + Stethoscope, + Search, + Copy, + Eye, + Edit, + Calendar, + Hash, + User, + Building2, + Cake +} from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { Link, LoaderFunctionArgs, useLoaderData, useNavigate } from 'react-router-dom' import { PatientModel } from 'src/shared/models/PatientModel' @@ -25,9 +43,14 @@ import { Input } from '@renderer/components/ui/input' import { cn, formatDate, formatDateTime } from '@renderer/lib/utils' import womenIcon from '../../../../../resources/woman.png?asset' import manIcon from '../../../../../resources/man.png?asset' -import { LabeledChip } from '@renderer/components/common/LabeledChip' -import { SectionHeader } from '@renderer/components/common/SectionHeader' -import { Toggle } from '@renderer/components/ui/toggle' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' +import { Badge } from '@renderer/components/ui/badge' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger +} from '@renderer/components/ui/accordion' export const loader = (queryClient: QueryClient) => @@ -40,80 +63,201 @@ export const loader = const surgeryListQuery = (filter: SurgeryFilter) => queryOptions({ ...queries.surgeries.list(filter) }) -interface PatientInfoCardProps { +// ============================================================ +// PatientSidebar - Comprehensive patient info card +// ============================================================ +interface PatientSidebarProps { patient: PatientModel - className?: string + onEdit: () => void } -const PatientInfoCard = ({ patient }: PatientInfoCardProps) => { - const [showRemarks, setShowRemarks] = useState(false) +const PatientSidebar = ({ patient, onEdit }: PatientSidebarProps) => { + const genderColor = patient.gender === 'M' ? 'bg-blue-500' : 'bg-pink-500' + const hasRemarks = !!patient.remarks + + // Demographics items + const demographicItems = [ + { + icon: Hash, + label: 'PHN', + value: patient.phn, + iconBg: 'bg-blue-500/10', + iconColor: 'text-blue-500', + mono: true + }, + { + icon: Cake, + label: 'Age', + value: `${patient.age > 1 ? patient.age : '<1'} years`, + iconBg: 'bg-emerald-500/10', + iconColor: 'text-emerald-500' + }, + { + icon: User, + label: 'Gender', + value: patient.gender === 'M' ? 'Male' : 'Female', + iconBg: patient.gender === 'M' ? 'bg-blue-500/10' : 'bg-pink-500/10', + iconColor: patient.gender === 'M' ? 'text-blue-500' : 'text-pink-500' + }, + { + icon: Building2, + label: 'Ward', + value: patient.ward || 'Not assigned', + iconBg: 'bg-violet-500/10', + iconColor: 'text-violet-500' + } + ] + + // Contact items (only if they exist) + const contactItems = [ + patient.phone && { + icon: Phone, + label: 'Phone', + value: patient.phone, + iconBg: 'bg-emerald-500/10', + iconColor: 'text-emerald-500' + }, + patient.emergency_contact && { + icon: UserPlus, + label: 'Emergency Contact', + value: patient.emergency_contact, + iconBg: 'bg-amber-500/10', + iconColor: 'text-amber-500' + }, + patient.emergency_phone && { + icon: PhoneCall, + label: 'Emergency Phone', + value: patient.emergency_phone, + iconBg: 'bg-red-500/10', + iconColor: 'text-red-500' + } + ].filter(Boolean) as { + icon: typeof Phone + label: string + value: string + iconBg: string + iconColor: string + }[] + + const hasContactInfo = contactItems.length > 0 return ( -
    -
    -
    -
    - Patient + + {/* Header with avatar */} +
    + {/* Decorative background */} +
    + +
    + {/* Avatar */} +
    +
    +
    + Patient +
    +
    +
    + {patient.gender} +
    +
    + + {/* Name and edit */} +
    +

    {patient.name || 'Unknown'}

    +

    {patient.phn}

    + +
    +
    -
    -
    -
    - - - {patient.age > 1 ? patient.age : '<1'} yrs} /> - + {/* Demographics */} + + {demographicItems.map((item, index) => ( +
    +
    +
    -
    - - - - +
    +

    {item.label}

    +

    {item.value}

    - {patient.remarks && ( -
    -
    - setShowRemarks(!showRemarks)} variant={'outline'}> - + ))} + - {showRemarks ? 'Hide' : 'Show'} Remarks - + {/* Contact info */} + {hasContactInfo && ( + +

    Contact

    + {contactItems.map((item, index) => ( +
    +
    + +
    +
    +

    {item.label}

    +

    {item.value}

    - {showRemarks && ( -
    -
    -
    - )}
    - )} + ))} + + )} + + {/* Remarks */} + {hasRemarks && ( +
    + + + +
    +
    + +
    + Remarks +
    +
    + +
    +
    +
    +
    +
    + + +
    -
    -
    + )} + ) } +// ============================================================ +// Enhanced surgery table columns +// ============================================================ // eslint-disable-next-line @typescript-eslint/no-explicit-any -const columns: ColumnDef[] = [ +const surgeryColumns: ColumnDef[] = [ { id: 'bht', header: 'BHT', cell: (cell) => ( {cell.row.original.bht} @@ -121,11 +265,11 @@ const columns: ColumnDef[] = [ }, { id: 'title', - header: 'Title', + header: 'Procedure', cell: (cell) => ( {cell.row.original.title} @@ -134,17 +278,31 @@ const columns: ColumnDef[] = [ { id: 'ward', header: 'Ward', - cell: (cell) => cell.row.original.ward + cell: (cell) => + cell.row.original.ward ? ( + + {cell.row.original.ward} + + ) : ( + + ) }, { id: 'date', header: 'Surgery Date', - cell: (cell) => (cell.row.original.date !== null ? formatDate(cell.row.original.date) : 'N/A') + cell: (cell) => ( +
    + + {cell.row.original.date !== null ? formatDate(cell.row.original.date) : 'N/A'} +
    + ) }, { id: 'updatedAt', - header: 'Updated At', - cell: (cell) => formatDateTime(cell.row.original.updated_at) + header: 'Updated', + cell: (cell) => ( + {formatDateTime(cell.row.original.updated_at)} + ) }, { id: 'actions', @@ -167,14 +325,19 @@ const columns: ColumnDef[] = [ toast.success('BHT copied to clipboard') }} > + Copy BHT - - View Surgery + + + + View Surgery + - + + Edit Surgery @@ -185,12 +348,15 @@ const columns: ColumnDef[] = [ } ] -interface PatientSurgeryListProps { +// ============================================================ +// PatientSurgeriesCard - Surgeries list with search +// ============================================================ +interface PatientSurgeriesCardProps { patientId: number - className?: string + onAddSurgery: () => void } -const PatientSurgeryList = ({ patientId, className }: PatientSurgeryListProps) => { +const PatientSurgeriesCard = ({ patientId, onAddSurgery }: PatientSurgeriesCardProps) => { const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 @@ -205,24 +371,69 @@ const PatientSurgeryList = ({ patientId, className }: PatientSurgeryListProps) = }) ) + const totalRecords = data?.total ?? 0 + return ( -
    - -
    - setSearch(e.target.value)} /> -
    - -
    + + +
    + +
    + +
    + Surgeries + + {totalRecords} + +
    + +
    + {/* Search input */} +
    + + setSearch(e.target.value)} + className="pl-9 h-9" + /> +
    +
    + + {!isLoading && totalRecords === 0 && !search ? ( + // Empty state +
    +
    + +
    +

    No surgeries yet

    +

    + This patient does not have any surgery records. +

    + +
    + ) : ( + + )} +
    +
    ) } +// ============================================================ +// Main ViewPatient Component +// ============================================================ export const ViewPatient = () => { const navigate = useNavigate() const { setBreadcrumbs } = useBreadcrumbs() @@ -238,32 +449,21 @@ export const ViewPatient = () => { setBreadcrumbs([{ label: 'Patients', to: '/patients' }, { label: ptName }]) }, [ptName, setBreadcrumbs]) - const actions = ( - <> - - - - ) + const handleAddSurgery = () => navigate(`/patients/${id}/surgeries/add`) + const handleEdit = () => navigate(`/patients/${id}/edit`) return ( - -
    - - + +
    + {/* Left Sidebar - Patient Info */} +
    + +
    + + {/* Main Content - Surgeries */} +
    + +
    ) From 5641c212fa32e71e7cd471c532464ff015d53341 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 00:04:15 +0530 Subject: [PATCH 06/26] feat: redesign patient edit form with card-based layout - Restructure form into card sections: Identity, Demographics, Location, Contact, Notes - Add gradient backgrounds and staggered entrance animations - Add icon badges to card headers with semantic color coding - Update Save button to gradient style with icon - Update Reset button to outline style with icon - Add dynamic gender icon coloring (blue for male, pink for female) - Enhance birth year preview with styled badge --- .../src/components/patient/NewPatientForm.tsx | 483 +++++++++++------- .../src/routes/patients/add-new-patient.tsx | 12 +- .../src/routes/patients/edit-patient.tsx | 12 +- 3 files changed, 328 insertions(+), 179 deletions(-) diff --git a/src/renderer/src/components/patient/NewPatientForm.tsx b/src/renderer/src/components/patient/NewPatientForm.tsx index 4745695..088eb54 100644 --- a/src/renderer/src/components/patient/NewPatientForm.tsx +++ b/src/renderer/src/components/patient/NewPatientForm.tsx @@ -23,6 +23,17 @@ import { } from '../ui/form' import toast from 'react-hot-toast' import { RichTextEditor } from '../common/RichTextEditor' +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card' +import { cn } from '@renderer/lib/utils' +import { + Hash, + User, + Cake, + MapPin, + Phone, + FileText, + Calendar +} from 'lucide-react' export interface CreatePatientFormProps { onRecordUpdated?: (patient: Patient) => void @@ -112,7 +123,9 @@ export const NewPatientForm = forwardRef approximateBirthYear(watchAge), [watchAge]) + const resetForm = useCallback(() => { if (isUpdate) { form.reset({ @@ -132,7 +145,7 @@ export const NewPatientForm = forwardRef resetForm() })) + // Dynamic gender icon colors + const genderIconBg = watchGender === 'M' ? 'bg-blue-500/10' : watchGender === 'F' ? 'bg-pink-500/10' : 'bg-muted' + const genderIconColor = watchGender === 'M' ? 'text-blue-500' : watchGender === 'F' ? 'text-pink-500' : 'text-muted-foreground' + return (
    - -
    - ( - - PHN - - - - - PHN of patient is a unique field usually comes with the admission - - - - )} - /> - - ( - - Name - - - - Name can be used to lookup patient in future - - - )} - /> + + {/* Row 1: Identity + Demographics */} +
    + {/* Identity Card */} + + +
    +
    + +
    + + Identity + +
    +
    + + {/* PHN Field */} + ( + +
    +
    + +
    + PHN +
    + + + + + Unique identifier from admission + + +
    + )} + /> + + {/* Name Field */} + ( + +
    +
    + +
    + Name +
    + + + + + Used for patient lookup + + +
    + )} + /> +
    +
    + + {/* Demographics Card */} + + +
    +
    + +
    + + Demographics + +
    +
    + + {/* Age Field */} + ( + +
    +
    + +
    + Age +
    + + + + {birthYear ? ( +
    + + + Birth year:{' '} + + {birthYear} + + +
    + ) : ( + + Format: 20y, 1y 6m, or 5m + + )} + +
    + )} + /> + + {/* Gender Field */} + ( + +
    +
    + +
    + Gender +
    + + +
    + )} + /> +
    +
    -
    - ( - - Age - - - - - {birthYear ? ( - - Birth year is approximately {birthYear} - - ) : ( - - Enter the age we will calculate the birth year of the patient. -
    If the user is - 20years old enter 20y. If the patient is{' '} - 1 year 6 months old enter 1y 6m -
    - )} -
    - -
    - )} - /> - - ( - - Gender - - - Male - Female - - - - - )} - /> -
    - - ( - - Address - - - - - - )} - /> - -
    - ( - - Phone - - - - - - )} - /> - - ( - - Emergency Contact - - - - - - )} - /> - - ( - - Emergency Phone - - - - - - )} - /> -
    - ( - - Remarks - - - - - Any additional remarks about the patient - - - - )} - /> + + + )} + /> + + + + {/* Row 3: Contact */} + + +
    +
    + +
    + + Contact Information + +
    +
    + +
    + {/* Phone Field */} + ( + + Phone + + + + + + )} + /> + + {/* Emergency Contact Field */} + ( + + Emergency Contact + + + + + + )} + /> + + {/* Emergency Phone Field */} + ( + + Emergency Phone + + + + + + )} + /> +
    +
    +
    + + {/* Row 4: Notes */} + + +
    +
    + +
    + + Notes + +
    +
    + + ( + + Remarks + + + + + Any additional remarks about the patient + + + + )} + /> + +
    ) diff --git a/src/renderer/src/routes/patients/add-new-patient.tsx b/src/renderer/src/routes/patients/add-new-patient.tsx index b9fafe7..bd11a1e 100644 --- a/src/renderer/src/routes/patients/add-new-patient.tsx +++ b/src/renderer/src/routes/patients/add-new-patient.tsx @@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom' import { Patient } from 'src/shared/types/db' import { NewPatientFormRef } from '../../components/patient/NewPatientForm' import { Button } from '@renderer/components/ui/button' -import { SaveIcon } from 'lucide-react' +import { RotateCcw, SaveIcon } from 'lucide-react' import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' export const AddNewPatient = () => { @@ -30,13 +30,19 @@ export const AddNewPatient = () => { const actions = ( <> - diff --git a/src/renderer/src/routes/patients/edit-patient.tsx b/src/renderer/src/routes/patients/edit-patient.tsx index d71e2e5..84486c0 100644 --- a/src/renderer/src/routes/patients/edit-patient.tsx +++ b/src/renderer/src/routes/patients/edit-patient.tsx @@ -8,7 +8,7 @@ import { Patient } from 'src/shared/types/db' import { queries } from '../../lib/queries' import { AppLayout } from '@renderer/layouts/AppLayout' import { Button } from '@renderer/components/ui/button' -import { SaveIcon } from 'lucide-react' +import { RotateCcw, SaveIcon } from 'lucide-react' import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' export const getPatientByIdQuery = (id: number) => @@ -41,13 +41,19 @@ export const EditPatient = () => { const actions = ( <> - From 5b1222527d3a55436cb36db1f43f4fea18597592 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 00:27:29 +0530 Subject: [PATCH 07/26] feat: redesign patients list with card layout and sorting - Replace table with card-based patient rows showing avatar, name, gender badge - Add search bar with integrated sort controls - Implement sorting by name, PHN, age, created/updated date - Add toggle for ascending/descending order - Update patient repository to support sorting options - Improve empty state and pagination UI --- src/main/repository/patient.ts | 36 +- .../src/routes/patients/list-patients.tsx | 466 +++++++++++++----- src/shared/types/api.d.ts | 2 + 3 files changed, 381 insertions(+), 123 deletions(-) diff --git a/src/main/repository/patient.ts b/src/main/repository/patient.ts index 9c6f0c5..8b8ccad 100644 --- a/src/main/repository/patient.ts +++ b/src/main/repository/patient.ts @@ -88,9 +88,10 @@ export const findPatientByPHN = async (phn: string) => { } export const listPatients = async (filter: PatientFilter) => { - const { search, pageSize = 25, page = 0 } = filter + const { search, pageSize = 25, page = 0, sortBy = 'updated_at', sortOrder = 'desc' } = filter let query = db.selectFrom('patients') + if (search) { const term = `${search}*` query = query @@ -110,14 +111,33 @@ export const listPatients = async (filter: PatientFilter) => { (join) => join.onRef('matching_patients.patient_id', '=', 'patients.id') ) .where('matching_patients.patient_id', 'is not', null) - .orderBy('rank', 'desc') } - const patients = await query - .selectAll('patients') - .orderBy('created_at desc') - .limit(pageSize) - .offset(page * pageSize) - .execute() + + // Apply sorting based on sortBy option + // For age, sort by birth_year in reverse (lower birth_year = older age) + let sortedQuery = query.selectAll('patients') + + switch (sortBy) { + case 'name': + sortedQuery = sortedQuery.orderBy('patients.name', sortOrder) + break + case 'phn': + sortedQuery = sortedQuery.orderBy('patients.phn', sortOrder) + break + case 'age': + // Reverse sort order for age since lower birth_year = older + sortedQuery = sortedQuery.orderBy('patients.birth_year', sortOrder === 'asc' ? 'desc' : 'asc') + break + case 'created_at': + sortedQuery = sortedQuery.orderBy('patients.created_at', sortOrder) + break + case 'updated_at': + default: + sortedQuery = sortedQuery.orderBy('patients.updated_at', sortOrder) + break + } + + const patients = await sortedQuery.limit(pageSize).offset(page * pageSize).execute() const mappedPatients = patients.map((patient) => new PatientModel(patient)) diff --git a/src/renderer/src/routes/patients/list-patients.tsx b/src/renderer/src/routes/patients/list-patients.tsx index e6a8da2..7e99be0 100644 --- a/src/renderer/src/routes/patients/list-patients.tsx +++ b/src/renderer/src/routes/patients/list-patients.tsx @@ -1,26 +1,57 @@ import { Button } from '@renderer/components/ui/button' -import { DataTable } from '@renderer/components/ui/data-table/data-table' +import { Card, CardContent } from '@renderer/components/ui/card' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@renderer/components/ui/dropdown-menu' import { Input } from '@renderer/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@renderer/components/ui/select' +import { Skeleton } from '@renderer/components/ui/skeleton' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { queries } from '@renderer/lib/queries' +import { cn } from '@renderer/lib/utils' import { QueryClient, keepPreviousData, queryOptions, useQuery } from '@tanstack/react-query' -import { ColumnDef, PaginationState } from '@tanstack/react-table' -import { MoreHorizontal, PlusSquare } from 'lucide-react' +import { + ArrowDownAZ, + ArrowUpAZ, + ChevronLeft, + ChevronRight, + Copy, + Edit3, + Eye, + FileSearch, + MoreVertical, + Plus, + Scissors, + Search, + Users +} from 'lucide-react' import { useEffect, useState } from 'react' import toast from 'react-hot-toast' -import { Link, LoaderFunctionArgs, useNavigate, useSearchParams } from 'react-router-dom' +import { LoaderFunctionArgs, useNavigate, useSearchParams } from 'react-router-dom' import { PatientModel } from 'src/shared/models/PatientModel' import { PatientFilter } from 'src/shared/types/api' +type SortBy = NonNullable +type SortOrder = NonNullable + +const SORT_OPTIONS: { value: SortBy; label: string }[] = [ + { value: 'updated_at', label: 'Last Updated' }, + { value: 'created_at', label: 'Date Added' }, + { value: 'name', label: 'Name' }, + { value: 'phn', label: 'PHN' }, + { value: 'age', label: 'Age' } +] + const patientListQuery = (filter: PatientFilter) => queryOptions({ ...queries.patients.list(filter), @@ -38,134 +69,339 @@ export const loader = return { q, page, pageSize } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const columns: ColumnDef[] = [ - { - id: 'phn', - header: 'PHN', - cell: (cell) => ( - - {cell.row.original.phn} - - ) - }, - { - id: 'name', - header: 'Name', - cell: (cell) => cell.row.original.name - }, - { - id: 'age', - header: 'Age', - cell: (cell) => `${cell.row.original.age > 0 ? cell.row.original.age : '<1'}y` - }, - { - id: 'gender', - header: 'Gender', - cell: (cell) => cell.row.original.gender - }, - { - id: 'updatedAt', - header: 'Updated At', - cell: (cell) => cell.row.original.updated_at.toLocaleDateString() - }, - { - id: 'actions', - cell: (cell) => { - const patient = cell.row.original - - return ( - - - - - - Actions - { - navigator.clipboard.writeText(patient.phn) - toast.success('PHN copied to clipboard') - }} - > - Copy PHN - - - - View Patient - - - Edit Patient - - - - Add Surgery - - - - ) - } - } -] +// Patient initials avatar +function PatientAvatar({ name, gender }: { name: string; gender: string }) { + const initials = name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) + + const bgColor = gender === 'M' ? 'bg-blue-500/15 text-blue-600' : 'bg-rose-500/15 text-rose-600' + + return ( +
    + {initials} +
    + ) +} + +// Gender badge +function GenderBadge({ gender }: { gender: string }) { + const isMale = gender === 'M' + return ( + + {isMale ? 'Male' : 'Female'} + + ) +} + +// Patient row component +function PatientRow({ + patient, + onNavigate +}: { + patient: PatientModel + onNavigate: (path: string) => void +}) { + return ( +
    onNavigate(`/patients/${patient.id}`)} + > + + +
    +
    + {patient.name} + +
    +
    + {patient.phn} + | + {patient.age > 0 ? `${patient.age} years` : '<1 year'} +
    +
    + +
    +
    Updated
    +
    {patient.updated_at.toLocaleDateString()}
    +
    + + + e.stopPropagation()}> + + + + { + e.stopPropagation() + navigator.clipboard.writeText(patient.phn) + toast.success('PHN copied to clipboard') + }} + > + + Copy PHN + + + { + e.stopPropagation() + onNavigate(`/patients/${patient.id}`) + }} + > + + View Patient + + { + e.stopPropagation() + onNavigate(`/patients/${patient.id}/edit`) + }} + > + + Edit Patient + + + { + e.stopPropagation() + onNavigate(`/patients/${patient.id}/surgeries/add`) + }} + > + + Add Surgery + + + +
    + ) +} + +// Loading skeleton for patient row +function PatientRowSkeleton() { + return ( +
    + +
    + + +
    + +
    + ) +} + +// Empty state +function EmptyState({ search }: { search: string }) { + return ( +
    +
    + +
    +

    No patients found

    +

    + {search + ? `No results for "${search}". Try adjusting your search terms.` + : 'Get started by adding your first patient to the system.'} +

    +
    + ) +} + +// Pagination controls +function Pagination({ + page, + pageSize, + total, + onPageChange +}: { + page: number + pageSize: number + total: number + onPageChange: (page: number) => void +}) { + const totalPages = Math.ceil(total / pageSize) + const start = page * pageSize + 1 + const end = Math.min((page + 1) * pageSize, total) + + if (total === 0) return null + + return ( +
    + + Showing {start} to{' '} + {end} of{' '} + {total} patients + +
    + + + Page {page + 1} of {totalPages} + + +
    +
    + ) +} export const PatientsIndex = () => { const [searchParams] = useSearchParams() const { setBreadcrumbs } = useBreadcrumbs() const navigate = useNavigate() - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10 - }) + const [page, setPage] = useState(0) + const [pageSize] = useState(10) const [search, setSearch] = useState(searchParams.get('q') || '') + const [sortBy, setSortBy] = useState('updated_at') + const [sortOrder, setSortOrder] = useState('desc') + const { data, isLoading } = useQuery( patientListQuery({ search, - page: pagination.pageIndex, - pageSize: pagination.pageSize + page, + pageSize, + sortBy, + sortOrder }) ) - const actions = ( -
    - -
    - ) - useEffect(() => { setBreadcrumbs([{ label: 'Patients', to: '/patients' }]) }, [setBreadcrumbs]) + const toggleSortOrder = () => { + setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')) + setPage(0) + } + return ( - -
    -
    - setSearch(e.target.value)} - /> +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Patients

    +

    {data?.total ?? 0} patients registered

    +
    - +
    - + + {/* Search & Sort Bar */} + + +
    + {/* Search Input */} +
    + + { + setSearch(e.target.value) + setPage(0) + }} + /> +
    + + {/* Sort Controls */} +
    + Sort by + + +
    +
    +
    +
    + + {/* Patient List */} +
    + {isLoading ? ( + <> + {[...Array(5)].map((_, i) => ( + + ))} + + ) : data?.data.length === 0 ? ( + + ) : ( + data?.data.map((patient) => ( + + )) + )} +
    + + {/* Pagination */} + {!isLoading && data && data.total > 0 && ( + + )} +
    ) } diff --git a/src/shared/types/api.d.ts b/src/shared/types/api.d.ts index 884a2c8..682fc6d 100644 --- a/src/shared/types/api.d.ts +++ b/src/shared/types/api.d.ts @@ -1,5 +1,7 @@ export interface PatientFilter { search?: string + sortBy?: 'name' | 'phn' | 'age' | 'updated_at' | 'created_at' + sortOrder?: 'asc' | 'desc' pageSize?: number page?: number From 7fe851d5c6979803faf28ea3dc19825953abe975 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 00:44:26 +0530 Subject: [PATCH 08/26] feat: redesign doctors list with card layout and sorting - Replace table with card-based doctor rows showing avatar and designation - Add search bar with integrated sort controls - Implement sorting by name, designation, created/updated date - Add toggle for ascending/descending order - Update doctor repository to support sorting options - Improve empty state, pagination, and delete confirmation UI --- .claude/settings.json | 5 + src/main/repository/doctor.ts | 27 +- .../src/routes/doctors/list-doctors.tsx | 474 +++++++++++++----- src/shared/types/api.d.ts | 2 + 4 files changed, 386 insertions(+), 122 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9030888 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true + } +} diff --git a/src/main/repository/doctor.ts b/src/main/repository/doctor.ts index db6ede3..9fb5561 100644 --- a/src/main/repository/doctor.ts +++ b/src/main/repository/doctor.ts @@ -50,7 +50,7 @@ export const deleteDoctorById = async (id: number) => { } export const listDoctors = async (filter: DoctorFilter) => { - const { search, pageSize = 25, page = 0 } = filter + const { search, pageSize = 25, page = 0, sortBy = 'updated_at', sortOrder = 'desc' } = filter let query = db.selectFrom('doctors').selectAll('doctors') @@ -59,14 +59,27 @@ export const listDoctors = async (filter: DoctorFilter) => { query = query .innerJoin('doctors_fts', 'doctors_fts.doctor_id', 'doctors.id') .where(sql`doctors_fts MATCH ${term}`) - .orderBy(sql`rank`, 'desc') } - const doctorResult = await query - .orderBy('updated_at desc') - .limit(pageSize) - .offset(page * pageSize) - .execute() + // Apply sorting + let sortedQuery = query + switch (sortBy) { + case 'name': + sortedQuery = sortedQuery.orderBy('doctors.name', sortOrder) + break + case 'designation': + sortedQuery = sortedQuery.orderBy('doctors.designation', sortOrder) + break + case 'created_at': + sortedQuery = sortedQuery.orderBy('doctors.created_at', sortOrder) + break + case 'updated_at': + default: + sortedQuery = sortedQuery.orderBy('doctors.updated_at', sortOrder) + break + } + + const doctorResult = await sortedQuery.limit(pageSize).offset(page * pageSize).execute() const totalResult = await query .clearSelect() diff --git a/src/renderer/src/routes/doctors/list-doctors.tsx b/src/renderer/src/routes/doctors/list-doctors.tsx index 80a8bc9..1e47d17 100644 --- a/src/renderer/src/routes/doctors/list-doctors.tsx +++ b/src/renderer/src/routes/doctors/list-doctors.tsx @@ -10,13 +10,19 @@ import { AlertDialogTrigger } from '@renderer/components/ui/alert-dialog' import { Button } from '@renderer/components/ui/button' -import { DataTable } from '@renderer/components/ui/data-table/data-table' - +import { Card, CardContent } from '@renderer/components/ui/card' import { Input } from '@renderer/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@renderer/components/ui/select' +import { Skeleton } from '@renderer/components/ui/skeleton' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { queries } from '@renderer/lib/queries' -import { formatDate, unwrapResult } from '@renderer/lib/utils' +import { cn, unwrapResult } from '@renderer/lib/utils' import { keepPreviousData, queryOptions, @@ -24,153 +30,391 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' -import { ColumnDef, PaginationState } from '@tanstack/react-table' -import { EditIcon, PlusSquare, Trash } from 'lucide-react' +import { + ArrowDownAZ, + ArrowUpAZ, + ChevronLeft, + ChevronRight, + Edit3, + FileSearch, + Plus, + Search, + Stethoscope, + Trash2, + UserCog +} from 'lucide-react' import { useEffect, useState } from 'react' import toast from 'react-hot-toast' - -import { Link, useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { DoctorModel } from 'src/shared/models/DoctorModel' - import { DoctorFilter } from 'src/shared/types/api' +type SortBy = NonNullable +type SortOrder = NonNullable + +const SORT_OPTIONS: { value: SortBy; label: string }[] = [ + { value: 'updated_at', label: 'Last Updated' }, + { value: 'created_at', label: 'Date Added' }, + { value: 'name', label: 'Name' }, + { value: 'designation', label: 'Designation' } +] + const doctorListQuery = (filter: DoctorFilter) => queryOptions({ ...queries.doctors.list(filter), placeholderData: keepPreviousData }) -const DoctorAction = ({ doctor }: { doctor: DoctorModel }) => { - const queryClient = useQueryClient() - const deleteDoctorMutation = useMutation({ - mutationFn: () => unwrapResult(window.api.invoke('deleteDoctorById', doctor.id)), - onSuccess: () => { - queryClient.invalidateQueries(queries.doctors.list({})) - }, - onError: (_error) => { - toast.error( - 'Failed to delete doctor. Please try again. This doctor may have associated records.' - ) - } - }) +// Doctor initials avatar +function DoctorAvatar({ name }: { name: string }) { + const initials = name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) return ( -
    - - - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the doctor. - - - - Cancel - - - - - - +
    + {initials}
    ) } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const columns: ColumnDef[] = [ - { - id: 'name', - header: 'Name', - cell: (cell) => ( - - {cell.row.original.name} - - ) - }, - { - id: 'designation', - header: 'Designation', - cell: (cell) => cell.row.original.designation - }, - { - id: 'updatedAt', - header: 'Updated At', - cell: (cell) => formatDate(cell.row.original.updated_at) - }, - { - id: 'actions', - cell: (cell) => - } -] +// Designation badge +function DesignationBadge({ designation }: { designation: string | null }) { + if (!designation) return null + return ( + + {designation} + + ) +} + +// Delete confirmation dialog +function DeleteDoctorDialog({ + doctor, + onDelete +}: { + doctor: DoctorModel + onDelete: () => void +}) { + return ( + + + + + e.stopPropagation()}> + + Delete Doctor + + Are you sure you want to delete {doctor.name}? This action cannot be + undone. + + + + Cancel + + + + + + + ) +} + +// Doctor row component +function DoctorRow({ + doctor, + onNavigate, + onDelete +}: { + doctor: DoctorModel + onNavigate: (path: string) => void + onDelete: () => void +}) { + return ( +
    onNavigate(`/doctors/${doctor.id}/edit`)} + > + + +
    +
    + {doctor.name} + +
    +
    + + Medical Professional +
    +
    + +
    +
    Updated
    +
    {doctor.updated_at.toLocaleDateString()}
    +
    + +
    + + +
    +
    + ) +} + +// Loading skeleton for doctor row +function DoctorRowSkeleton() { + return ( +
    + +
    + + +
    + +
    + ) +} + +// Empty state +function EmptyState({ search }: { search: string }) { + return ( +
    +
    + +
    +

    No doctors found

    +

    + {search + ? `No results for "${search}". Try adjusting your search terms.` + : 'Get started by adding your first doctor to the system.'} +

    +
    + ) +} + +// Pagination controls +function Pagination({ + page, + pageSize, + total, + onPageChange +}: { + page: number + pageSize: number + total: number + onPageChange: (page: number) => void +}) { + const totalPages = Math.ceil(total / pageSize) + const start = page * pageSize + 1 + const end = Math.min((page + 1) * pageSize, total) + + if (total === 0) return null + + return ( +
    + + Showing {start} to{' '} + {end} of{' '} + {total} doctors + +
    + + + Page {page + 1} of {totalPages} + + +
    +
    + ) +} export const DoctorsIndex = () => { const { setBreadcrumbs } = useBreadcrumbs() const navigate = useNavigate() + const queryClient = useQueryClient() - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10 - }) + const [page, setPage] = useState(0) + const [pageSize] = useState(10) const [search, setSearch] = useState('') + const [sortBy, setSortBy] = useState('updated_at') + const [sortOrder, setSortOrder] = useState('desc') + const { data, isLoading } = useQuery( doctorListQuery({ search, - page: pagination.pageIndex, - pageSize: pagination.pageSize + page, + pageSize, + sortBy, + sortOrder }) ) - const actions = ( -
    - -
    - ) + const deleteDoctorMutation = useMutation({ + mutationFn: (id: number) => unwrapResult(window.api.invoke('deleteDoctorById', id)), + onSuccess: () => { + queryClient.invalidateQueries(queries.doctors.list({})) + toast.success('Doctor deleted successfully') + }, + onError: () => { + toast.error('Failed to delete doctor. This doctor may have associated records.') + } + }) useEffect(() => { setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }]) }, [setBreadcrumbs]) + const toggleSortOrder = () => { + setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')) + setPage(0) + } + return ( - -
    -
    - setSearch(e.target.value)} - /> +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Doctors

    +

    {data?.total ?? 0} doctors registered

    +
    - +
    - + + {/* Search & Sort Bar */} + + +
    + {/* Search Input */} +
    + + { + setSearch(e.target.value) + setPage(0) + }} + /> +
    + + {/* Sort Controls */} +
    + Sort by + + +
    +
    +
    +
    + + {/* Doctor List */} +
    + {isLoading ? ( + <> + {[...Array(5)].map((_, i) => ( + + ))} + + ) : data?.data.length === 0 ? ( + + ) : ( + data?.data.map((doctor) => ( + deleteDoctorMutation.mutate(doctor.id)} + /> + )) + )} +
    + + {/* Pagination */} + {!isLoading && data && data.total > 0 && ( + + )} +
    ) } diff --git a/src/shared/types/api.d.ts b/src/shared/types/api.d.ts index 682fc6d..6e62c77 100644 --- a/src/shared/types/api.d.ts +++ b/src/shared/types/api.d.ts @@ -20,6 +20,8 @@ export interface SurgeryFilter { export interface DoctorFilter { search?: string + sortBy?: 'name' | 'designation' | 'updated_at' | 'created_at' + sortOrder?: 'asc' | 'desc' pageSize?: number page?: number From d9feb1e798c2398eeb75c8e39ffbe7fb38e8ab4c Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 00:48:16 +0530 Subject: [PATCH 09/26] feat: redesign surgeries list with card layout and sorting - Replace table with card-based surgery rows showing icon, title, ward badge - Add search bar with integrated sort controls - Implement sorting by date, title, BHT, ward, created/updated date - Add toggle for ascending/descending order - Update surgery repository to support sorting options - Improve empty state, pagination, and delete confirmation UI - Use emerald color theme for surgery-related elements --- src/main/repository/surgery.ts | 44 +- .../src/routes/surgeries/list-surgeries.tsx | 505 +++++++++++++----- src/shared/types/api.d.ts | 2 + 3 files changed, 407 insertions(+), 144 deletions(-) diff --git a/src/main/repository/surgery.ts b/src/main/repository/surgery.ts index cb5d8e6..f499abf 100644 --- a/src/main/repository/surgery.ts +++ b/src/main/repository/surgery.ts @@ -293,7 +293,17 @@ export const lookupSurgery = async (search: string) => { } export const listSurgeries = async (filter: SurgeryFilter) => { - const { search, ward, start_date, end_date, patient_id, pageSize = 50, page = 0 } = filter + const { + search, + ward, + start_date, + end_date, + patient_id, + pageSize = 50, + page = 0, + sortBy = 'date', + sortOrder = 'desc' + } = filter let query = db.selectFrom('surgeries').selectAll('surgeries') @@ -306,12 +316,10 @@ export const listSurgeries = async (filter: SurgeryFilter) => { .selectFrom('surgeries_fts') .select(['surgeries_fts.surgery_id', sql`rank`.as('rank')]) .where(sql`surgeries_fts MATCH ${term}`) - .as('matching_patients'), (join) => join.onRef('matching_patients.surgery_id', '=', 'surgeries.id') ) .where('matching_patients.surgery_id', 'is not', null) - .orderBy('rank', 'desc') } if (patient_id) { @@ -330,11 +338,31 @@ export const listSurgeries = async (filter: SurgeryFilter) => { query = query.where('surgeries.date', '<=', end_date) } - const surgeries = await query - .orderBy('date', 'asc') - .limit(pageSize) - .offset(page * pageSize) - .execute() + // Apply sorting + let sortedQuery = query + switch (sortBy) { + case 'title': + sortedQuery = sortedQuery.orderBy('surgeries.title', sortOrder) + break + case 'bht': + sortedQuery = sortedQuery.orderBy('surgeries.bht', sortOrder) + break + case 'ward': + sortedQuery = sortedQuery.orderBy('surgeries.ward', sortOrder) + break + case 'created_at': + sortedQuery = sortedQuery.orderBy('surgeries.created_at', sortOrder) + break + case 'updated_at': + sortedQuery = sortedQuery.orderBy('surgeries.updated_at', sortOrder) + break + case 'date': + default: + sortedQuery = sortedQuery.orderBy('surgeries.date', sortOrder) + break + } + + const surgeries = await sortedQuery.limit(pageSize).offset(page * pageSize).execute() const totalResult = await query .clearSelect() diff --git a/src/renderer/src/routes/surgeries/list-surgeries.tsx b/src/renderer/src/routes/surgeries/list-surgeries.tsx index 833a0de..4981cc7 100644 --- a/src/renderer/src/routes/surgeries/list-surgeries.tsx +++ b/src/renderer/src/routes/surgeries/list-surgeries.tsx @@ -9,17 +9,22 @@ import { AlertDialogTitle, AlertDialogTrigger } from '@renderer/components/ui/alert-dialog' -import { Button, buttonVariants } from '@renderer/components/ui/button' -import { DataTable } from '@renderer/components/ui/data-table/data-table' - +import { Button } from '@renderer/components/ui/button' +import { Card, CardContent } from '@renderer/components/ui/card' import { Input } from '@renderer/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@renderer/components/ui/select' +import { Skeleton } from '@renderer/components/ui/skeleton' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { queries } from '@renderer/lib/queries' -import { cn, formatDate, formatDateTime, unwrapResult } from '@renderer/lib/utils' +import { cn, unwrapResult } from '@renderer/lib/utils' import { SurgeryModel } from '@shared/models/SurgeryModel' import { SurgeryFilter } from '@shared/types/api' - import { keepPreviousData, queryOptions, @@ -27,12 +32,34 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' -import { ColumnDef, PaginationState } from '@tanstack/react-table' -import { ArrowRight, EditIcon, Trash } from 'lucide-react' +import { + ArrowDownAZ, + ArrowUpAZ, + Calendar, + ChevronLeft, + ChevronRight, + Edit3, + FileSearch, + Plus, + Search, + Stethoscope, + Trash2 +} from 'lucide-react' import { useEffect, useState } from 'react' import toast from 'react-hot-toast' +import { useNavigate } from 'react-router-dom' + +type SortBy = NonNullable +type SortOrder = NonNullable -import { Link } from 'react-router-dom' +const SORT_OPTIONS: { value: SortBy; label: string }[] = [ + { value: 'date', label: 'Surgery Date' }, + { value: 'updated_at', label: 'Last Updated' }, + { value: 'created_at', label: 'Date Added' }, + { value: 'title', label: 'Title' }, + { value: 'bht', label: 'BHT' }, + { value: 'ward', label: 'Ward' } +] const surgeryListQuery = (filter: SurgeryFilter) => queryOptions({ @@ -40,156 +67,362 @@ const surgeryListQuery = (filter: SurgeryFilter) => placeholderData: keepPreviousData }) -const SurgeryAction = ({ surgery }: { surgery: SurgeryModel }) => { - const queryClient = useQueryClient() - const deleteDoctorMutation = useMutation({ - mutationFn: () => unwrapResult(window.api.invoke('deleteSurgeryById', surgery.id)), - onSuccess: () => { - queryClient.invalidateQueries(queries.surgeries.list({})) - }, - onError: (_error) => { - toast.error( - 'Failed to delete surgery. Please try again. This surgery may have associated records.' - ) - } - }) +// Surgery icon with color based on ward or type +function SurgeryIcon() { + return ( +
    + +
    + ) +} +// Ward badge +function WardBadge({ ward }: { ward: string }) { return ( -
    - - - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the surgery. - - - - Cancel - - - - - - + + {ward} + + ) +} + +// Date badge +function DateBadge({ date }: { date: Date | null }) { + if (!date) return null + return ( + + + {date.toLocaleDateString()} + + ) +} + +// Delete confirmation dialog +function DeleteSurgeryDialog({ + surgery, + onDelete +}: { + surgery: SurgeryModel + onDelete: () => void +}) { + return ( + + + + + e.stopPropagation()}> + + Delete Surgery + + Are you sure you want to delete surgery {surgery.title || surgery.bht}? + This will also delete all follow-up records. This action cannot be undone. + + + + Cancel + + + + + + + ) +} + +// Surgery row component +function SurgeryRow({ + surgery, + onNavigate, + onDelete +}: { + surgery: SurgeryModel + onNavigate: (path: string) => void + onDelete: () => void +}) { + return ( +
    onNavigate(`/patients/${surgery.patient_id}/surgeries/${surgery.id}`)} + > + + +
    +
    + + {surgery.title || 'Untitled Surgery'} + + +
    +
    + {surgery.bht} + | + +
    +
    + +
    +
    Updated
    +
    {surgery.updated_at.toLocaleDateString()}
    +
    + +
    + + +
    ) } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const columns: ColumnDef[] = [ - { - id: 'bht', - header: 'BHT', - cell: (cell) => ( - - {cell.row.original.bht} - - ) - }, - { - id: 'title', - header: 'Title', - cell: (cell) => ( - - {cell.row.original.title} - - ) - }, - { - id: 'surgeryDate', - header: 'Surgery Date', - cell: (cell) => (cell.row.original.date ? formatDate(cell.row.original.date) : 'N/A') - }, - { - id: 'updatedAt', - header: 'Updated At', - cell: (cell) => formatDateTime(cell.row.original.updated_at) - }, - { - id: 'patient', - header: 'Patient', - cell: (cell) => ( - - Go to Patient - - ) - }, - { - id: 'actions', - header: 'Actions', - cell: (cell) => - } -] +// Loading skeleton for surgery row +function SurgeryRowSkeleton() { + return ( +
    + +
    + + +
    + +
    + ) +} + +// Empty state +function EmptyState({ search }: { search: string }) { + return ( +
    +
    + +
    +

    No surgeries found

    +

    + {search + ? `No results for "${search}". Try adjusting your search terms.` + : 'No surgery records yet. Add a surgery from a patient profile.'} +

    +
    + ) +} + +// Pagination controls +function Pagination({ + page, + pageSize, + total, + onPageChange +}: { + page: number + pageSize: number + total: number + onPageChange: (page: number) => void +}) { + const totalPages = Math.ceil(total / pageSize) + const start = page * pageSize + 1 + const end = Math.min((page + 1) * pageSize, total) + + if (total === 0) return null + + return ( +
    + + Showing {start} to{' '} + {end} of{' '} + {total} surgeries + +
    + + + Page {page + 1} of {totalPages} + + +
    +
    + ) +} export const SurgierisIndex = () => { const { setBreadcrumbs } = useBreadcrumbs() + const navigate = useNavigate() + const queryClient = useQueryClient() - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10 - }) + const [page, setPage] = useState(0) + const [pageSize] = useState(10) const [search, setSearch] = useState('') + const [sortBy, setSortBy] = useState('date') + const [sortOrder, setSortOrder] = useState('desc') + const { data, isLoading } = useQuery( surgeryListQuery({ search, - page: pagination.pageIndex, - pageSize: pagination.pageSize + page, + pageSize, + sortBy, + sortOrder }) ) + const deleteSurgeryMutation = useMutation({ + mutationFn: (id: number) => unwrapResult(window.api.invoke('deleteSurgeryById', id)), + onSuccess: () => { + queryClient.invalidateQueries(queries.surgeries.list({})) + toast.success('Surgery deleted successfully') + }, + onError: () => { + toast.error('Failed to delete surgery. Please try again.') + } + }) + useEffect(() => { setBreadcrumbs([{ label: 'Surgeries', to: '/surgeries' }]) }, [setBreadcrumbs]) + const toggleSortOrder = () => { + setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')) + setPage(0) + } + return ( - -
    -
    - setSearch(e.target.value)} - /> +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Surgeries

    +

    {data?.total ?? 0} surgery records

    +
    - +
    - + + {/* Search & Sort Bar */} + + +
    + {/* Search Input */} +
    + + { + setSearch(e.target.value) + setPage(0) + }} + /> +
    + + {/* Sort Controls */} +
    + Sort by + + +
    +
    +
    +
    + + {/* Surgery List */} +
    + {isLoading ? ( + <> + {[...Array(5)].map((_, i) => ( + + ))} + + ) : data?.data.length === 0 ? ( + + ) : ( + data?.data.map((surgery) => ( + deleteSurgeryMutation.mutate(surgery.id)} + /> + )) + )} +
    + + {/* Pagination */} + {!isLoading && data && data.total > 0 && ( + + )} +
    ) } diff --git a/src/shared/types/api.d.ts b/src/shared/types/api.d.ts index 6e62c77..695fdca 100644 --- a/src/shared/types/api.d.ts +++ b/src/shared/types/api.d.ts @@ -13,6 +13,8 @@ export interface SurgeryFilter { start_date?: Date end_date?: Date patient_id?: number + sortBy?: 'title' | 'bht' | 'date' | 'ward' | 'updated_at' | 'created_at' + sortOrder?: 'asc' | 'desc' pageSize?: number page?: number From f89047e6f35d9ef6aab81904283eb21c06b23ab8 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 01:08:52 +0530 Subject: [PATCH 10/26] feat: redesign surgery form with card layout and prominent shortcut hint - Reorganize form into card-based sections (Procedure, Details, Dates, Team, Notes) - Add prominent emerald-themed shortcut hint banner with clickable insert button - Update add/edit surgery pages with consistent emerald-themed headers - Remove redundant labels from rich text areas (card headers are sufficient) - Clean up duplicate field icons for cleaner visual hierarchy --- .../src/components/patient/NewPatientForm.tsx | 4 - .../components/surgery/AddOrEditSurgery.tsx | 431 ++++++++++++------ .../src/routes/surgeries/add-surgery.tsx | 70 +-- .../src/routes/surgeries/edit-surgery.tsx | 83 ++-- 4 files changed, 375 insertions(+), 213 deletions(-) diff --git a/src/renderer/src/components/patient/NewPatientForm.tsx b/src/renderer/src/components/patient/NewPatientForm.tsx index 088eb54..33cf9e5 100644 --- a/src/renderer/src/components/patient/NewPatientForm.tsx +++ b/src/renderer/src/components/patient/NewPatientForm.tsx @@ -526,13 +526,9 @@ export const NewPatientForm = forwardRef ( - Remarks - - Any additional remarks about the patient - )} diff --git a/src/renderer/src/components/surgery/AddOrEditSurgery.tsx b/src/renderer/src/components/surgery/AddOrEditSurgery.tsx index b23d286..2e7d053 100644 --- a/src/renderer/src/components/surgery/AddOrEditSurgery.tsx +++ b/src/renderer/src/components/surgery/AddOrEditSurgery.tsx @@ -14,24 +14,30 @@ import { Input } from '../ui/input' import { DatePicker } from '../ui/date-picker' import { RichTextEditor } from '../common/RichTextEditor' -import { forwardRef, useEffect, useImperativeHandle } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react' import { DoctorAutoComplete } from '../doctor/DoctorAutoComplete' import toast from 'react-hot-toast' import { SurgeryModel } from '../../../../shared/models/SurgeryModel' import { unwrapResult } from '@renderer/lib/utils' +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card' +import { Button } from '../ui/button' +import { + Stethoscope, + FileText, + Calendar, + Users, + Hash, + Lightbulb, + ClipboardPlus +} from 'lucide-react' const toValidDate = (date?: Date | null): Date | null => { if (!date) return null return !isNaN(date.getTime()) ? date : null } -const KBD = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) const surgerySchema = z.object({ - title: z.string().min(1, { message: 'Title is required' }), // Add a custom error message for min(1 + title: z.string().min(1, { message: 'Title is required' }), bht: z.string().min(1, { message: 'BHT is required' }), ward: z.string().min(1, { message: 'Ward is required' }), date: z.date().nullable(), @@ -57,6 +63,7 @@ export interface AddOrEditSurgeryRef { export const AddOrEditSurgery = forwardRef( ({ surgery, patientId, onUpdated }: AddOrEditSurgeryProps, ref) => { + const titleInputRef = useRef(null) const form = useForm({ resolver: zodResolver(surgerySchema), defaultValues: { @@ -165,12 +172,23 @@ export const AddOrEditSurgery = forwardRef { + const currentTitle = form.getValues('title') + form.setValue('title', currentTitle + '↓') + // Focus the input and move cursor to end + if (titleInputRef.current) { + titleInputRef.current.focus() + const len = titleInputRef.current.value.length + titleInputRef.current.setSelectionRange(len, len) + } + }, [form]) + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowDown' && e.ctrlKey && e.shiftKey) { e.preventDefault() - // Insert ↓ to surgery title - form.setValue('title', form.getValues('title') + '↓') + insertDownArrow() } } @@ -179,171 +197,283 @@ export const AddOrEditSurgery = forwardRef { window.removeEventListener('keydown', handleKeyDown) } - }, [form]) + }, [insertDownArrow]) return (
    - - {/* Surgery Details */} -
    - ( - - Title - - - - + + {/* Procedure Title Card */} + + +
    +
    + +
    + + Procedure + +
    +
    + + ( + + Surgery Title + + { + field.ref(e) + // @ts-expect-error ref assignment + titleInputRef.current = e + }} + /> + + + + )} + /> + + {/* Prominent Shortcut Hint */} +
    +
    + +
    +
    +

    + Pro tip: Add down arrow (↓) to title +

    +

    Press{' '} - - Ctrl - - + - - Shift - - + - - - {' '} - to add an ↓ - - - - )} - /> - -

    -
    + + Ctrl + + {' + '} + + Shift + + {' + '} + + ↓ + + {' or click the button'} +

    +
    + +
    + + + + {/* Details Card - BHT & Ward */} + + +
    +
    + +
    + + Details + +
    +
    + +
    ( - BHT + BHT - + + + Bed Head Ticket number + )} /> -
    -
    ( - Ward + Ward - + )} />
    -
    - -
    - ( - - Surgery Date - - - - - - )} - /> - ( - - Date of Admission - - - - - - )} - /> - ( - - Date of Discharge - - - - - - )} - /> -
    - -
    -
    -
    - ( - - Done By - - { - field.onChange(ids.map((id) => +id)) - }} - selectedDoctorIds={field.value.map((v) => v.toString())} - /> - - - - )} - /> + + + + {/* Dates Card */} + + +
    +
    +
    - -
    - ( - - Assisted By - - { - field.onChange(ids.map((id) => +id)) - }} - selectedDoctorIds={field.value.map((v) => v.toString())} - /> - - - - )} - /> + + Dates + +
    + + +
    + ( + + Surgery Date + + + + + + )} + /> + ( + + Date of Admission + + + + + + )} + /> + ( + + Date of Discharge + + + + + + )} + /> +
    +
    + + + {/* Surgical Team Card */} + + +
    +
    +
    + + Surgical Team +
    -
    +
    + +
    + ( + + Done By + + { + field.onChange(ids.map((id) => +id)) + }} + selectedDoctorIds={field.value.map((v) => v.toString())} + /> + + + Primary surgeon(s) + + + + )} + /> -
    + ( + + Assisted By + + { + field.onChange(ids.map((id) => +id)) + }} + selectedDoctorIds={field.value.map((v) => v.toString())} + /> + + + Assisting surgeon(s) + + + + )} + /> +
    + + + + {/* Notes Card */} + + +
    +
    + +
    + + Operative Notes + +
    +
    + ( - Notes )} /> -
    - -
    + + + + {/* Post-Op Notes Card */} + + +
    +
    + +
    + + Post-Operative Care + +
    +
    + ( - Post Op Notes )} /> -
    -
    + +
    ) diff --git a/src/renderer/src/routes/surgeries/add-surgery.tsx b/src/renderer/src/routes/surgeries/add-surgery.tsx index 69490a3..90ba968 100644 --- a/src/renderer/src/routes/surgeries/add-surgery.tsx +++ b/src/renderer/src/routes/surgeries/add-surgery.tsx @@ -1,8 +1,7 @@ import { Button } from '@renderer/components/ui/button' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { Save } from 'lucide-react' +import { Save, Stethoscope } from 'lucide-react' import { useEffect, useMemo, useRef } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { getPatientByIdQuery } from '../patients/edit-patient' @@ -47,34 +46,45 @@ export const AddNewSurgery = () => { } }, [setBreadcrumbs, patient, ptName]) - const actions = ( - <> - - - ) - return ( - - {patient && ( - { - await queryClient.invalidateQueries({ - queryKey: queries.surgeries.list({ patient_id: parseInt(patientId!) }).queryKey - }) - navigate(`/patients/${patient.id}/surgeries/${surgery.id}`) - }} - /> - )} - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Add Surgery

    +

    + Recording new surgery for {ptName} +

    +
    +
    + +
    + + {/* Form Content */} +
    + {patient && ( + { + await queryClient.invalidateQueries({ + queryKey: queries.surgeries.list({ patient_id: parseInt(patientId!) }).queryKey + }) + navigate(`/patients/${patient.id}/surgeries/${surgery.id}`) + }} + /> + )} +
    +
    ) } diff --git a/src/renderer/src/routes/surgeries/edit-surgery.tsx b/src/renderer/src/routes/surgeries/edit-surgery.tsx index 6450e84..7690f9d 100644 --- a/src/renderer/src/routes/surgeries/edit-surgery.tsx +++ b/src/renderer/src/routes/surgeries/edit-surgery.tsx @@ -1,8 +1,7 @@ import { Button } from '@renderer/components/ui/button' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { Save } from 'lucide-react' +import { Save, Stethoscope } from 'lucide-react' import { useEffect, useMemo, useRef } from 'react' import { useParams } from 'react-router-dom' import { getPatientByIdQuery } from '../patients/edit-patient' @@ -57,40 +56,52 @@ export const EditSurgery = () => { } }, [setBreadcrumbs, patient, ptName, surgeryName, surgeryId]) - const actions = ( - <> - - - ) - return ( - - {patient && surgery && ( - { - await queryClient.invalidateQueries({ - queryKey: queries.surgeries.list({ patient_id: parseInt(patientId!) }).queryKey - }) - await queryClient.invalidateQueries({ - queryKey: queries.surgeries.get(parseInt(surgeryId!)).queryKey - }) - await queryClient.invalidateQueries({ - queryKey: queries.patients.get(patient.id).queryKey - }) - }} - /> - )} - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Edit Surgery

    +

    + Editing {surgeryName} for{' '} + {ptName} +

    +
    +
    + +
    + + {/* Form Content */} +
    + {patient && surgery && ( + { + await queryClient.invalidateQueries({ + queryKey: queries.surgeries.list({ patient_id: parseInt(patientId!) }).queryKey + }) + await queryClient.invalidateQueries({ + queryKey: queries.surgeries.get(parseInt(surgeryId!)).queryKey + }) + await queryClient.invalidateQueries({ + queryKey: queries.patients.get(patient.id).queryKey + }) + }} + /> + )} +
    +
    ) } From d14ebac67ec2a047cef59fd55f3212b12bd166ad Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 01:10:50 +0530 Subject: [PATCH 11/26] feat: make patient name clickable in surgery add/edit pages Add navigation link to patient view from surgery form headers --- src/renderer/src/routes/surgeries/add-surgery.tsx | 10 ++++++++-- src/renderer/src/routes/surgeries/edit-surgery.tsx | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/routes/surgeries/add-surgery.tsx b/src/renderer/src/routes/surgeries/add-surgery.tsx index 90ba968..7151c0e 100644 --- a/src/renderer/src/routes/surgeries/add-surgery.tsx +++ b/src/renderer/src/routes/surgeries/add-surgery.tsx @@ -3,7 +3,7 @@ import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Save, Stethoscope } from 'lucide-react' import { useEffect, useMemo, useRef } from 'react' -import { useNavigate, useParams } from 'react-router-dom' +import { Link, useNavigate, useParams } from 'react-router-dom' import { getPatientByIdQuery } from '../patients/edit-patient' import { AddOrEditSurgery, @@ -57,7 +57,13 @@ export const AddNewSurgery = () => {

    Add Surgery

    - Recording new surgery for {ptName} + Recording new surgery for{' '} + + {ptName} +

    diff --git a/src/renderer/src/routes/surgeries/edit-surgery.tsx b/src/renderer/src/routes/surgeries/edit-surgery.tsx index 7690f9d..692e9d9 100644 --- a/src/renderer/src/routes/surgeries/edit-surgery.tsx +++ b/src/renderer/src/routes/surgeries/edit-surgery.tsx @@ -3,7 +3,7 @@ import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Save, Stethoscope } from 'lucide-react' import { useEffect, useMemo, useRef } from 'react' -import { useParams } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' import { getPatientByIdQuery } from '../patients/edit-patient' import { AddOrEditSurgery, @@ -68,7 +68,12 @@ export const EditSurgery = () => {

    Edit Surgery

    Editing {surgeryName} for{' '} - {ptName} + + {ptName} +

    From e754aba47febd4f710f9c275b2abb5f9e3ddb6c6 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 01:20:52 +0530 Subject: [PATCH 12/26] feat: redesign view surgery page and update sheet components - Redesign view surgery with card-based layout matching other pages - Add emerald-themed header with clickable patient link - Organize surgery details, team, notes into visual cards - Update follow-up sheet with cyan-themed header and wider width - Update add doctor sheet with blue-themed header - Add gradient buttons and better visual hierarchy to all sheets --- .../components/doctor/DoctorAutoComplete.tsx | 50 +- .../components/surgery/AddOrEditFollowup.tsx | 48 +- .../src/routes/surgeries/view-surgery.tsx | 648 +++++++++++------- 3 files changed, 459 insertions(+), 287 deletions(-) diff --git a/src/renderer/src/components/doctor/DoctorAutoComplete.tsx b/src/renderer/src/components/doctor/DoctorAutoComplete.tsx index 19d776a..6ceac7a 100644 --- a/src/renderer/src/components/doctor/DoctorAutoComplete.tsx +++ b/src/renderer/src/components/doctor/DoctorAutoComplete.tsx @@ -5,14 +5,13 @@ import { queryOptions, useQueries, useQuery } from '@tanstack/react-query' import { queries } from '../../lib/queries' import { useCallback, useRef, useState } from 'react' import { Badge } from '../ui/badge' -import { X } from 'lucide-react' +import { X, UserPlus, Save } from 'lucide-react' import { Sheet, - SheetClose, SheetContent, - SheetDescription, SheetFooter, - SheetHeader + SheetHeader, + SheetTitle } from '../ui/sheet' import { Button } from '../ui/button' import { AddOrEditDoctor, AddOrEditDoctorRef } from './AddOrEditDoctor' @@ -53,7 +52,7 @@ const SelectedDoctorChips = ({ return (
    - {results.map((result, _i) => { + {results.map((result) => { const doctor = result.data if (!doctor) return null return ( @@ -127,25 +126,36 @@ export const DoctorAutoComplete = ({ onSelected, selectedDoctorIds }: DoctorAuto
    - setAddNewSheetOpen(false)}> - - Add New Doctor - Add a new doctor to the system + + + +
    +
    + +
    +
    + Add New Doctor +

    + Add a new doctor to the system +

    +
    +
    +
    -
    +
    - - - - + + diff --git a/src/renderer/src/components/surgery/AddOrEditFollowup.tsx b/src/renderer/src/components/surgery/AddOrEditFollowup.tsx index 6241f92..420b836 100644 --- a/src/renderer/src/components/surgery/AddOrEditFollowup.tsx +++ b/src/renderer/src/components/surgery/AddOrEditFollowup.tsx @@ -2,7 +2,6 @@ import { Followup } from 'src/shared/types/db' import { Sheet, SheetContent, - SheetDescription, SheetFooter, SheetHeader, SheetTitle, @@ -18,6 +17,7 @@ import { unwrapResult } from '@renderer/lib/utils' import toast from 'react-hot-toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { queries } from '@renderer/lib/queries' +import { ChevronRight, Save } from 'lucide-react' export interface AddOrEditFollowupProps { surgeryId?: number @@ -27,13 +27,13 @@ export interface AddOrEditFollowupProps { trigger?: React.ReactNode } -const followupSchema = z.object({ +const _followupSchema = z.object({ notes: z.string().min(1, { message: 'Note is required' }) }) -type FollowupSchema = z.infer +type FollowupSchema = z.infer export const AddOrEditFollowup = ({ trigger, @@ -111,7 +111,7 @@ export const AddOrEditFollowup = ({ } else { await createNewFollowup(values) } - } catch (error) { + } catch { toast.error('Failed to save followup') } } @@ -121,23 +121,32 @@ export const AddOrEditFollowup = ({ return ( {trigger} - +
    - - {!isUpdate ? <>Add Followup : <>Edit Followup} - - {!isUpdate ? ( - <>Add a new followup for this surgery - ) : ( - <>Edit the followup for this surgery - )} - + +
    +
    + +
    +
    + + {!isUpdate ? 'Add Follow-up' : 'Edit Follow-up'} + +

    + {!isUpdate + ? 'Record a new follow-up note for this surgery' + : 'Update the follow-up details'} +

    +
    +
    +
    +
    ( - + )} /> - - +
    + + diff --git a/src/renderer/src/routes/surgeries/view-surgery.tsx b/src/renderer/src/routes/surgeries/view-surgery.tsx index b9d1b94..9144924 100644 --- a/src/renderer/src/routes/surgeries/view-surgery.tsx +++ b/src/renderer/src/routes/surgeries/view-surgery.tsx @@ -1,17 +1,30 @@ import { Button } from '@renderer/components/ui/button' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Edit, PlusIcon, Printer, Trash } from 'lucide-react' +import { + Edit, + PlusIcon, + Printer, + Trash, + Stethoscope, + Hash, + Building2, + Calendar, + Users, + FileText, + ClipboardPlus, + Clock, + ChevronRight +} from 'lucide-react' import { useEffect, useMemo } from 'react' -import { useNavigate, useParams } from 'react-router-dom' +import { Link, useNavigate, useParams } from 'react-router-dom' import { getPatientByIdQuery } from '../patients/edit-patient' import { queries } from '@renderer/lib/queries' import { SurgeryModel } from 'src/shared/models/SurgeryModel' import { PatientModel } from 'src/shared/models/PatientModel' import { Badge } from '@renderer/components/ui/badge' import { AddOrEditFollowup } from '@renderer/components/surgery/AddOrEditFollowup' -import { Card, CardContent, CardHeader } from '@renderer/components/ui/card' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { cn, formatDate, formatDateTime, isEmptyHtml, unwrapResult } from '@renderer/lib/utils' import { Skeleton } from '@renderer/components/ui/skeleton' import { FollowupModel } from 'src/shared/models/FollowupModel' @@ -34,24 +47,91 @@ import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' const getSurgeryByIdQuery = (id: number) => queries.surgeries.get(id) const getSurgeryFollowupsQuery = (surgeryId: number) => queries.surgeries.getFollowups(surgeryId) -export interface SurgeryCardProps { - surgery: SurgeryModel - patient: PatientModel - settings?: Record -} +// ============================================================ +// Rich Text Content Display +// ============================================================ +const RichTextContent = ({ content, className }: { content: string; className?: string }) => ( +
    +) -const FollowupShimmer = () => { - return ( -
    - -
    - - +// ============================================================ +// Empty State Component +// ============================================================ +const EmptyState = ({ message, icon: Icon }: { message: string; icon?: React.ElementType }) => ( +
    + {Icon && ( +
    +
    + )} +

    {message}

    +
    +) + +// ============================================================ +// Info Item Component +// ============================================================ +interface InfoItemProps { + icon: React.ElementType + label: string + value: string | null | undefined + iconBg: string + iconColor: string + mono?: boolean +} + +const InfoItem = ({ icon: Icon, label, value, iconBg, iconColor, mono }: InfoItemProps) => ( +
    +
    + +
    +
    +

    {label}

    +

    {value || 'N/A'}

    +
    +) + +// ============================================================ +// Doctor List Component +// ============================================================ +interface DoctorListProps { + doctors: DoctorModel[] | undefined + emptyMessage: string +} + +const DoctorList = ({ doctors, emptyMessage }: DoctorListProps) => { + if (!doctors || doctors.length === 0) { + return

    {emptyMessage}

    + } + + return ( +
      + {doctors.map((doctor) => ( +
    • +
      + + Dr. {doctor.name} + {doctor.designation && ( + ({doctor.designation}) + )} + +
    • + ))} +
    ) } +// ============================================================ +// Followup Card Component +// ============================================================ interface FollowupCardProps { followup: FollowupModel patient: PatientModel @@ -79,238 +159,83 @@ const FollowupCard = ({ followup, patient, surgery, settings }: FollowupCardProp } return ( - - - {formatDateTime(followup.created_at)} - - - - - - - } - followup={followup} - /> - - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the followup. - - - - Cancel - - - - - - - - - - - - ) -} - -const RichTextContent = ({ content, className }: { content: string; className?: string }) => ( -
    -) - -const NoContentCard = ({ - children, - className -}: { - children: React.ReactNode - className?: string -}) => ( - - {children} - -) - -interface DoctorListItemProps { - doctor: DoctorModel -} - -const DoctorListItem = ({ doctor }: DoctorListItemProps) => ( -
  • - Dr. {doctor.name} - {doctor.designation && <>({doctor.designation})} -
  • -) - -export const SurgeryCard = ({ surgery, patient, settings }: SurgeryCardProps) => { - const { data: followups, isLoading: isFollowupLoading } = useQuery({ - ...getSurgeryFollowupsQuery(surgery.id) - }) - - const noFollowups = !isFollowupLoading && followups && followups.length === 0 - - return ( -
    -
    -
    - Patient:{' '} - {patient.name} +
    + {/* Header */} +
    +
    + + {formatDateTime(followup.created_at)}
    - -
    - BHT:{' '} - {surgery.bht} -
    - -
    - Ward:{' '} - {surgery.ward} -
    - -
    - Date:{' '} - {surgery.date ? formatDate(surgery.date) : 'N/A'} -
    - - {surgery.doa && ( -
    - DoA:{' '} - {formatDate(surgery.doa)} -
    - )} - - {surgery.dod && ( -
    - DoD:{' '} - {formatDate(surgery.dod)} -
    - )} -
    - -
    -
    - Done By: - {surgery.doneBy && surgery.doneBy.length > 0 ? ( -
      - {surgery.doneBy?.map((doctor) => ( - - ))} -
    - ) : ( - N/A - )} -
    - -
    - Assisted By: - {surgery.assistedBy && surgery.assistedBy.length > 0 ? ( -
      - {surgery.assistedBy?.map((doctor) => ( - - ))} -
    - ) : ( - N/A - )} -
    -
    - -
    - Notes - - {!isEmptyHtml(surgery.notes) ? ( - - ) : ( - - No notes available - - )} -
    - -
    - Post Op Notes - - {!isEmptyHtml(surgery.post_op_notes) ? ( - - ) : ( - - No post op notes available - - )} -
    - -
    - - Followups +
    + - Add Followup + } - surgeryId={surgery.id} + followup={followup} /> - - -
    - {isFollowupLoading && } - - {noFollowups && ( - - No followups available - - )} - - {followups?.map((followup) => ( - - ))} + + + + + + + Delete Follow-up + + Are you sure you want to delete this follow-up? This action cannot be undone. + + + + Cancel + + + + + +
    + + {/* Content */} +
    ) } +// ============================================================ +// Followup Shimmer +// ============================================================ +const FollowupShimmer = () => ( +
    +
    + + +
    + +
    +) + +// ============================================================ +// Main View Surgery Component +// ============================================================ export const ViewSurgery = () => { const navigate = useNavigate() const { patientId, surgeryId } = useParams() const { setBreadcrumbs } = useBreadcrumbs() const { settings } = useSettings() + const { data: patient } = useQuery({ ...getPatientByIdQuery(parseInt(patientId!)), enabled: !!patientId @@ -321,6 +246,11 @@ export const ViewSurgery = () => { enabled: !!surgeryId }) + const { data: followups, isLoading: isFollowupLoading } = useQuery({ + ...getSurgeryFollowupsQuery(parseInt(surgeryId!)), + enabled: !!surgeryId + }) + const ptName = useMemo( () => patient?.name || patient?.phn || 'Patient', [patient?.name, patient?.phn] @@ -357,27 +287,247 @@ export const ViewSurgery = () => { onKeyDown: handlePrint }) - const actions = ( - <> - - - - ) + const noFollowups = !isFollowupLoading && followups && followups.length === 0 return ( - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    {surgeryName}

    +

    + Surgery record for{' '} + + {ptName} + +

    +
    +
    +
    + + +
    +
    + + {/* Main Content */} {patient && surgery && ( - +
    + {/* Top Row: Details + Team */} +
    + {/* Surgery Details Card */} + + +
    +
    + +
    + + Surgery Details + +
    +
    + +
    + + + + + +
    +
    +
    + + {/* Surgical Team Card */} + + +
    +
    + +
    + + Surgical Team + +
    +
    + +
    +

    + Done By +

    + +
    +
    +

    + Assisted By +

    + +
    +
    +
    +
    + + {/* Notes Row */} +
    + {/* Operative Notes Card */} + + +
    +
    + +
    + + Operative Notes + +
    +
    + + {!isEmptyHtml(surgery.notes) ? ( +
    + +
    + ) : ( + + )} +
    +
    + + {/* Post-Op Notes Card */} + + +
    +
    + +
    + + Post-Operative Care + +
    +
    + + {!isEmptyHtml(surgery.post_op_notes) ? ( +
    + +
    + ) : ( + + )} +
    +
    +
    + + {/* Follow-ups Card */} + + +
    +
    +
    + +
    +
    + + Follow-ups + + {followups && followups.length > 0 && ( + + {followups.length} + + )} +
    +
    + + + Add Follow-up + + } + surgeryId={surgery.id} + /> +
    +
    + + {isFollowupLoading && } + + {noFollowups && ( + + )} + + {followups && followups.length > 0 && ( +
    + {followups.map((followup) => ( + + ))} +
    + )} +
    +
    +
    )} - +
    ) } + +// Re-export for backward compatibility +export interface SurgeryCardProps { + surgery: SurgeryModel + patient: PatientModel + settings?: Record +} + +export const SurgeryCard = (_props: SurgeryCardProps) => { + // This is kept for backward compatibility but the main view now uses inline components + return null +} From 82ef50312885080c86fc9b70e033d8303936ebfe Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 01:39:03 +0530 Subject: [PATCH 13/26] feat: add quick add surgery workflow with patient lookup Adds a new Quick Add Surgery feature that allows users to start surgery creation directly from the home page with inline patient lookup and creation. This becomes the hero CTA on the home page. - Add PatientLookup component with debounced search - Add PatientResultCard for displaying search results - Add SelectedPatientSidebar for patient context during surgery form - Add quick-add-surgery wizard page with step navigation - Update home page hero CTA to Quick Add Surgery (emerald theme) - Update command palette and surgeries list to use new route --- .../command-palette/CommandPalette.tsx | 2 +- .../src/components/surgery/PatientLookup.tsx | 144 +++++++++ .../components/surgery/PatientResultCard.tsx | 108 +++++++ .../surgery/SelectedPatientSidebar.tsx | 112 +++++++ src/renderer/src/main.tsx | 5 + src/renderer/src/routes/home.tsx | 25 +- .../src/routes/surgeries/list-surgeries.tsx | 2 +- .../routes/surgeries/quick-add-surgery.tsx | 290 ++++++++++++++++++ 8 files changed, 675 insertions(+), 13 deletions(-) create mode 100644 src/renderer/src/components/surgery/PatientLookup.tsx create mode 100644 src/renderer/src/components/surgery/PatientResultCard.tsx create mode 100644 src/renderer/src/components/surgery/SelectedPatientSidebar.tsx create mode 100644 src/renderer/src/routes/surgeries/quick-add-surgery.tsx diff --git a/src/renderer/src/components/command-palette/CommandPalette.tsx b/src/renderer/src/components/command-palette/CommandPalette.tsx index 4a30150..7c67309 100644 --- a/src/renderer/src/components/command-palette/CommandPalette.tsx +++ b/src/renderer/src/components/command-palette/CommandPalette.tsx @@ -169,7 +169,7 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { Add New Patient - runCommand(() => navigate('/surgeries/add'))}> + runCommand(() => navigate('/quick-surgery'))}> Add New Surgery diff --git a/src/renderer/src/components/surgery/PatientLookup.tsx b/src/renderer/src/components/surgery/PatientLookup.tsx new file mode 100644 index 0000000..15945f8 --- /dev/null +++ b/src/renderer/src/components/surgery/PatientLookup.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { queries } from '@renderer/lib/queries' +import { useDebounce } from '@renderer/hooks/useDebounce' +import { Input } from '@renderer/components/ui/input' +import { Skeleton } from '@renderer/components/ui/skeleton' +import { PatientResultCard } from './PatientResultCard' +import { Patient } from 'src/shared/types/db' +import { Search, UserPlus, Users } from 'lucide-react' +import { Button } from '@renderer/components/ui/button' +import { cn } from '@renderer/lib/utils' + +interface PatientLookupProps { + onPatientSelect: (patient: Patient) => void + onCreatePatient: () => void + className?: string +} + +export function PatientLookup({ + onPatientSelect, + onCreatePatient, + className +}: PatientLookupProps) { + const [search, setSearch] = useState('') + const debouncedSearch = useDebounce(search, 200) + + const { data: patients, isLoading } = useQuery({ + ...queries.patients.list({ search: debouncedSearch, pageSize: 10, page: 0 }), + enabled: debouncedSearch.length > 0 + }) + + const hasResults = (patients?.data?.length ?? 0) > 0 + const showResults = debouncedSearch.length > 0 + + return ( +
    + {/* Search Input */} +
    + + setSearch(e.target.value)} + className="pl-12 h-14 text-lg bg-card border-2 focus:border-emerald-500 transition-colors" + autoFocus + /> +
    + + {/* Results Area */} +
    + {/* Loading State */} + {isLoading && debouncedSearch.length > 0 && ( +
    + {[1, 2, 3].map((i) => ( +
    + +
    + + +
    +
    + ))} +
    + )} + + {/* Results List */} + {!isLoading && showResults && hasResults && ( +
    +
    + + + {patients?.total ?? 0} patient{(patients?.total ?? 0) !== 1 ? 's' : ''} found + +
    +
    + {patients?.data?.map((patient, index) => ( + + ))} +
    +
    + )} + + {/* No Results State */} + {!isLoading && showResults && !hasResults && ( +
    +
    + +
    +

    No patients found

    +

    + No patients match “{debouncedSearch}”. Create a new patient to continue. +

    + +
    + )} + + {/* Empty State - Before Search */} + {!showResults && ( +
    +
    + +
    +

    Search for a patient

    +

    + Enter the patient's PHN, BHT number, or name to find their record +

    +
    + )} +
    + + {/* Create Patient Option - Always visible when there are results */} + {showResults && hasResults && ( +
    + +
    + )} +
    + ) +} diff --git a/src/renderer/src/components/surgery/PatientResultCard.tsx b/src/renderer/src/components/surgery/PatientResultCard.tsx new file mode 100644 index 0000000..f3e880d --- /dev/null +++ b/src/renderer/src/components/surgery/PatientResultCard.tsx @@ -0,0 +1,108 @@ +import { Patient } from 'src/shared/types/db' +import { cn } from '@renderer/lib/utils' +import { User, Hash, Calendar } from 'lucide-react' + +interface PatientResultCardProps { + patient: Patient + onSelect: (patient: Patient) => void + isSelected?: boolean + className?: string + style?: React.CSSProperties +} + +// Calculate age from birth year +function getAge(birthYear: number): string { + const currentYear = new Date().getFullYear() + const age = currentYear - birthYear + return `${age}y` +} + +export function PatientResultCard({ + patient, + onSelect, + isSelected, + className, + style +}: PatientResultCardProps) { + return ( + + ) +} diff --git a/src/renderer/src/components/surgery/SelectedPatientSidebar.tsx b/src/renderer/src/components/surgery/SelectedPatientSidebar.tsx new file mode 100644 index 0000000..ffb693d --- /dev/null +++ b/src/renderer/src/components/surgery/SelectedPatientSidebar.tsx @@ -0,0 +1,112 @@ +import { Patient } from 'src/shared/types/db' +import { cn } from '@renderer/lib/utils' +import { User, Hash, Calendar, MapPin, Phone, ArrowLeft } from 'lucide-react' +import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' + +interface SelectedPatientSidebarProps { + patient: Patient + onChangePatient: () => void + className?: string +} + +// Calculate age from birth year +function getAge(birthYear: number): string { + const currentYear = new Date().getFullYear() + const age = currentYear - birthYear + return `${age}y` +} + +export function SelectedPatientSidebar({ + patient, + onChangePatient, + className +}: SelectedPatientSidebarProps) { + return ( + + +
    +
    +
    + +
    + + Selected Patient + +
    + +
    +
    + + {/* Patient Name & Gender Badge */} +
    +

    {patient.name}

    + + {patient.gender === 'M' ? 'Male' : 'Female'} + +
    + + {/* Quick Info Grid */} +
    +
    + +
    +
    PHN
    +
    {patient.phn}
    +
    +
    +
    + +
    +
    Age
    +
    {getAge(patient.birth_year)}
    +
    +
    +
    + + {/* Additional Info */} + {(patient.address || patient.phone) && ( +
    + {patient.address && ( +
    + + {patient.address} +
    + )} + {patient.phone && ( +
    + + {patient.phone} +
    + )} +
    + )} +
    +
    + ) +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 8be5d05..e2833b3 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -22,6 +22,7 @@ import { DoctorsIndex } from './routes/doctors/list-doctors' import { AddNewDoctor } from './routes/doctors/add-doctor' import { EditDoctor } from './routes/doctors/edit-doctor' import { SurgierisIndex } from './routes/surgeries/list-surgeries' +import { QuickAddSurgery } from './routes/surgeries/quick-add-surgery' import { SettingsIndex } from './routes/settings/settings-index' import { AddTemplatePage, EditTemplatePage } from './routes/settings/template-form-page' @@ -90,6 +91,10 @@ const router = createHashRouter([ path: '/surgeries', element: }, + { + path: '/quick-surgery', + element: + }, { path: '/settings', diff --git a/src/renderer/src/routes/home.tsx b/src/renderer/src/routes/home.tsx index 024c19b..a98bf6e 100644 --- a/src/renderer/src/routes/home.tsx +++ b/src/renderer/src/routes/home.tsx @@ -147,19 +147,19 @@ function StatsCards({ stats, isLoading }: StatsCardsProps) { ) } -// Hero CTA - Add Patient card -interface AddPatientCardProps { +// Hero CTA - Quick Add Surgery card +interface QuickAddSurgeryCardProps { className?: string } -function AddPatientCard({ className }: AddPatientCardProps) { +function QuickAddSurgeryCard({ className }: QuickAddSurgeryCardProps) { const navigate = useNavigate() return (
    - diff --git a/src/renderer/src/routes/surgeries/quick-add-surgery.tsx b/src/renderer/src/routes/surgeries/quick-add-surgery.tsx new file mode 100644 index 0000000..5c7a1a2 --- /dev/null +++ b/src/renderer/src/routes/surgeries/quick-add-surgery.tsx @@ -0,0 +1,290 @@ +import { useEffect, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useQueryClient } from '@tanstack/react-query' +import { Patient } from 'src/shared/types/db' +import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' +import { PatientLookup } from '@renderer/components/surgery/PatientLookup' +import { SelectedPatientSidebar } from '@renderer/components/surgery/SelectedPatientSidebar' +import { + AddOrEditSurgery, + AddOrEditSurgeryRef +} from '@renderer/components/surgery/AddOrEditSurgery' +import { NewPatientForm, NewPatientFormRef } from '@renderer/components/patient/NewPatientForm' +import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' +import { queries } from '@renderer/lib/queries' +import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' +import { cn } from '@renderer/lib/utils' +import toast from 'react-hot-toast' +import { + Stethoscope, + Search, + UserPlus, + Save, + ArrowLeft, + ArrowRight, + X +} from 'lucide-react' + +type WizardStep = 'lookup' | 'create-patient' | 'surgery-form' + +export function QuickAddSurgery() { + const navigate = useNavigate() + const queryClient = useQueryClient() + const { setBreadcrumbs } = useBreadcrumbs() + const [step, setStep] = useState('lookup') + const [selectedPatient, setSelectedPatient] = useState(null) + + const surgeryFormRef = useRef(null) + const patientFormRef = useRef(null) + + // Set breadcrumbs based on current step + useEffect(() => { + const crumbs = [{ label: 'Quick Add Surgery' }] + if (step === 'create-patient') { + crumbs.push({ label: 'Create Patient' }) + } else if (step === 'surgery-form' && selectedPatient) { + crumbs.push({ label: selectedPatient.name }) + } + setBreadcrumbs(crumbs) + }, [setBreadcrumbs, step, selectedPatient]) + + // Handle keyboard shortcut for save + useKeyboardEvent({ + key: 's', + ctrlKey: true, + onKeyDown: () => { + if (step === 'surgery-form') { + surgeryFormRef.current?.submit() + } else if (step === 'create-patient') { + patientFormRef.current?.submit() + } + } + }) + + // Handle escape to go back + useKeyboardEvent({ + key: 'Escape', + onKeyDown: () => { + if (step === 'create-patient') { + setStep('lookup') + } else if (step === 'surgery-form') { + setStep('lookup') + setSelectedPatient(null) + } + } + }) + + const handlePatientSelect = (patient: Patient) => { + setSelectedPatient(patient) + setStep('surgery-form') + } + + const handleCreatePatient = () => { + setStep('create-patient') + } + + const handlePatientCreated = (patient: Patient) => { + toast.success('Patient created successfully') + setSelectedPatient(patient) + setStep('surgery-form') + } + + const handleChangePatient = () => { + setStep('lookup') + setSelectedPatient(null) + } + + const handleSurgeryCreated = async (surgery: { id: number }) => { + if (selectedPatient) { + await queryClient.invalidateQueries({ + queryKey: queries.surgeries.list({ patient_id: selectedPatient.id }).queryKey + }) + navigate(`/patients/${selectedPatient.id}/surgeries/${surgery.id}`) + } + } + + const getStepTitle = () => { + switch (step) { + case 'lookup': + return 'Find Patient' + case 'create-patient': + return 'Create Patient' + case 'surgery-form': + return 'Surgery Details' + } + } + + const getStepDescription = () => { + switch (step) { + case 'lookup': + return 'Search for an existing patient or create a new one' + case 'create-patient': + return 'Enter patient details to create a new record' + case 'surgery-form': + return `Recording surgery for ${selectedPatient?.name}` + } + } + + const getStepIcon = () => { + switch (step) { + case 'lookup': + return Search + case 'create-patient': + return UserPlus + case 'surgery-form': + return Stethoscope + } + } + + const StepIcon = getStepIcon() + + return ( +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    {getStepTitle()}

    +

    {getStepDescription()}

    +
    +
    +
    + {/* Back button for create-patient step */} + {step === 'create-patient' && ( + + )} + {/* Save button for create-patient step */} + {step === 'create-patient' && ( + + )} + {/* Save button for surgery-form step */} + {step === 'surgery-form' && ( + + )} + {/* Cancel button - always visible */} + +
    +
    + + {/* Step Indicator */} +
    + {['lookup', 'surgery-form'].map((s, i) => ( +
    + {i > 0 &&
    } +
    + + {step === 'surgery-form' && s === 'lookup' ? '✓' : i + 1} + + {s === 'lookup' ? 'Patient' : 'Surgery'} +
    +
    + ))} +
    + + {/* Main Content */} +
    + {/* Step 1: Patient Lookup */} + {step === 'lookup' && ( +
    + + +
    +
    + +
    + + Patient Lookup + +
    +
    + + + +
    +
    + )} + + {/* Step 1b: Create Patient (inline) */} + {step === 'create-patient' && ( +
    + +
    + )} + + {/* Step 2: Surgery Form with Patient Sidebar */} + {step === 'surgery-form' && selectedPatient && ( +
    + {/* Patient Sidebar */} +
    + +
    + {/* Surgery Form */} +
    + +
    +
    + )} +
    +
    + ) +} From 47c138dfec640c1325acf083e44e6356d3c0eff5 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 01:46:45 +0530 Subject: [PATCH 14/26] feat: redesign doctor add/edit pages with two-column layout - Consolidate form fields into single card with icon badges - Add two-column layout with helpful sidebar content - Add Doctor: tips sidebar explaining each field's purpose - Edit Doctor: current info preview showing existing values - Add keyboard shortcut hint (Ctrl+S to save) - Match visual style with other recently updated pages --- .../src/components/doctor/AddOrEditDoctor.tsx | 177 +++++++++++++----- .../src/routes/doctors/add-doctor.tsx | 123 ++++++++++-- .../src/routes/doctors/edit-doctor.tsx | 147 ++++++++++++--- 3 files changed, 355 insertions(+), 92 deletions(-) diff --git a/src/renderer/src/components/doctor/AddOrEditDoctor.tsx b/src/renderer/src/components/doctor/AddOrEditDoctor.tsx index e4a58a6..3b3666e 100644 --- a/src/renderer/src/components/doctor/AddOrEditDoctor.tsx +++ b/src/renderer/src/components/doctor/AddOrEditDoctor.tsx @@ -11,11 +11,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import { z } from 'zod' import { Input } from '../ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card' import { forwardRef, useImperativeHandle } from 'react' import toast from 'react-hot-toast' import { unwrapResult } from '@renderer/lib/utils' import { DoctorModel } from '@shared/models/DoctorModel' +import { User, Briefcase, Award, Stethoscope } from 'lucide-react' const doctorSchema = z.object({ name: z.string().min(1, { @@ -108,56 +110,131 @@ export const AddOrEditDoctor = forwardRef -
    - ( - - Name - - - - - - - )} - /> - -
    - ( - - Designation - - - - - - Designation of the doctor(use short form). This will be used when printing the - report - - - )} - /> -
    - -
    - ( - - SLMC Registration - - - - - - )} - /> + + {/* Main Doctor Info Card */} + + {/* Card Header with decorative element */} + +
    +
    +
    + +
    +
    + + Doctor Information + +

    + Basic details and credentials +

    +
    +
    + + + + {/* Name Field */} + ( + +
    +
    + +
    + Full Name +
    + + + + + As it should appear on printed reports + + +
    + )} + /> + + {/* Two column layout for Designation and SLMC */} +
    + {/* Designation Field */} + ( + +
    +
    + +
    + Designation +
    + + + + + Short form for printed reports + + +
    + )} + /> + + {/* SLMC Registration Field */} + ( + +
    +
    + +
    + SLMC Registration +
    + + + + + Optional - Medical Council number + + +
    + )} + /> +
    +
    + + + {/* Keyboard Shortcut Hint */} +
    + Press + + Ctrl + + + + + S + + to save
    diff --git a/src/renderer/src/routes/doctors/add-doctor.tsx b/src/renderer/src/routes/doctors/add-doctor.tsx index 2bdce4d..df3340e 100644 --- a/src/renderer/src/routes/doctors/add-doctor.tsx +++ b/src/renderer/src/routes/doctors/add-doctor.tsx @@ -1,11 +1,11 @@ import { AddOrEditDoctor, AddOrEditDoctorRef } from '@renderer/components/doctor/AddOrEditDoctor' import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' -import { AppLayout } from '@renderer/layouts/AppLayout' import { queries } from '@renderer/lib/queries' import { useQueryClient } from '@tanstack/react-query' -import { Save } from 'lucide-react' +import { Save, UserPlus, UserCog, Briefcase, Award, Lightbulb } from 'lucide-react' import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { DoctorModel } from 'src/shared/models/DoctorModel' @@ -15,7 +15,8 @@ export const AddNewDoctor = () => { const formRef = useRef(null) const queryClient = useQueryClient() const { setBreadcrumbs } = useBreadcrumbs() - const handleNewPatient = (_doctor: DoctorModel) => { + + const handleCreated = (_doctor: DoctorModel) => { navigate(`/doctors`) } @@ -27,27 +28,109 @@ export const AddNewDoctor = () => { } }) - const actions = ( - <> - - - ) - useEffect(() => { setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }, { label: 'Add Doctor' }]) }, [setBreadcrumbs]) return ( - - - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Add Doctor

    +

    + Register a new doctor to the system +

    +
    +
    + +
    + + {/* Main Content - Two Column Layout */} +
    +
    + {/* Left: Form */} +
    + +
    + + {/* Right: Tips Sidebar */} +
    + {/* Tips Card */} + + +
    +
    + +
    + + Quick Tips + +
    +
    + +
    +
    + +
    +
    +

    Full Name

    +

    + Include title (Dr.) for proper report formatting +

    +
    +
    + +
    +
    + +
    +
    +

    Designation

    +

    + Use abbreviations like GHO, MO, RMO, Consultant +

    +
    +
    + +
    +
    + +
    +
    +

    SLMC Number

    +

    + Optional but useful for official documentation +

    +
    +
    +
    +
    + + {/* Usage Info */} + + +

    + Doctors can be assigned to surgeries as the operating surgeon or assistant. Their details appear on printed surgery reports. +

    +
    +
    +
    +
    +
    +
    ) } diff --git a/src/renderer/src/routes/doctors/edit-doctor.tsx b/src/renderer/src/routes/doctors/edit-doctor.tsx index 41cf3f6..d8dcd4f 100644 --- a/src/renderer/src/routes/doctors/edit-doctor.tsx +++ b/src/renderer/src/routes/doctors/edit-doctor.tsx @@ -1,14 +1,16 @@ import { AddOrEditDoctor, AddOrEditDoctorRef } from '@renderer/components/doctor/AddOrEditDoctor' import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' -import { AppLayout } from '@renderer/layouts/AppLayout' import { queries } from '@renderer/lib/queries' +import { cn } from '@renderer/lib/utils' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { Save } from 'lucide-react' +import { Save, UserCog, Briefcase, Award, Calendar, Eye } from 'lucide-react' import { useEffect, useRef } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { DoctorModel } from 'src/shared/models/DoctorModel' +import { format } from 'date-fns' export const EditDoctor = () => { const { id } = useParams() @@ -19,7 +21,7 @@ export const EditDoctor = () => { const formRef = useRef(null) const { setBreadcrumbs } = useBreadcrumbs() - const handleNewPatient = (_doctor: DoctorModel) => { + const handleUpdated = (_doctor: DoctorModel) => { navigate(`/doctors`) } @@ -31,29 +33,130 @@ export const EditDoctor = () => { } }) - const actions = ( - <> - - - ) - useEffect(() => { setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }, { label: data?.name || 'Doctor' }]) }, [setBreadcrumbs, data?.name]) return ( - - {!isLoading && data && ( - - )} - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Edit Doctor

    +

    + Update details for {data?.name || 'doctor'} +

    +
    +
    + +
    + + {/* Main Content - Two Column Layout */} +
    +
    + {/* Left: Form */} +
    + {!isLoading && data && ( + + )} +
    + + {/* Right: Doctor Preview Sidebar */} +
    + {/* Current Info Preview */} + + +
    +
    +
    + +
    + + Current Info + +
    + + + {data && ( + <> + {/* Doctor Avatar and Name */} +
    +
    + +
    +
    +

    {data.name}

    +

    {data.designation || 'No designation'}

    +
    +
    + + {/* Details */} +
    +
    +
    + +
    +
    +

    Designation

    +

    + {data.designation || 'Not set'} +

    +
    +
    + +
    +
    + +
    +
    +

    SLMC Registration

    +

    + {data.slmc_reg_no || 'Not set'} +

    +
    +
    + +
    +
    + +
    +
    +

    Added

    +

    + {data.created_at ? format(new Date(data.created_at), 'MMM d, yyyy') : 'Unknown'} +

    +
    +
    +
    + + )} +
    + + + {/* Usage Info */} + + +

    + Changes will be reflected in all surgeries where this doctor is assigned as surgeon or assistant. +

    +
    +
    +
    +
    +
    +
    ) } From 77891260674da535b59ec4d9bb7e768ae2a66fb3 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 02:28:56 +0530 Subject: [PATCH 15/26] feat: redesign patient pages with modern header and two-column layout Update patient add, edit, and view pages to match the doctor page design: - Replace AppLayout wrapper with custom header structure - Add emerald-themed icon badges and animated headers - Two-column layout with form and info/tips sidebar - Current patient info preview on edit page - Quick tips sidebar on add page --- package.json | 1 + pnpm-lock.yaml | 31 + .../command-palette/CommandPalette.tsx | 5 +- .../components/settings/BackupSettings.tsx | 306 ++++---- .../components/settings/GeneralSettings.tsx | 215 ++++-- .../src/components/settings/TemplateForm.tsx | 695 +++++++++++------- .../components/settings/TemplatesSettings.tsx | 296 +++++--- src/renderer/src/components/ui/switch.tsx | 27 + .../src/routes/doctors/add-doctor.tsx | 2 +- .../src/routes/doctors/edit-doctor.tsx | 4 +- .../src/routes/patients/add-new-patient.tsx | 136 +++- .../src/routes/patients/edit-patient.tsx | 210 ++++-- .../src/routes/patients/list-patients.tsx | 101 ++- .../src/routes/patients/view-patient.tsx | 55 +- .../src/routes/settings/settings-index.tsx | 75 +- .../routes/settings/template-form-page.tsx | 253 ++++++- .../src/routes/surgeries/add-surgery.tsx | 10 +- .../src/routes/surgeries/edit-surgery.tsx | 11 +- .../routes/surgeries/quick-add-surgery.tsx | 12 +- .../src/routes/surgeries/view-surgery.tsx | 10 +- 20 files changed, 1624 insertions(+), 831 deletions(-) create mode 100644 src/renderer/src/components/ui/switch.tsx diff --git a/package.json b/package.json index 47946ba..48a8701 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83c28d0..d7f4221 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.1 version: 1.2.4(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.1.2 version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1280,6 +1283,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.1.13': resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: @@ -5617,6 +5633,21 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 diff --git a/src/renderer/src/components/command-palette/CommandPalette.tsx b/src/renderer/src/components/command-palette/CommandPalette.tsx index 7c67309..8555fc4 100644 --- a/src/renderer/src/components/command-palette/CommandPalette.tsx +++ b/src/renderer/src/components/command-palette/CommandPalette.tsx @@ -17,10 +17,7 @@ import { UserCog, Settings, Plus, - Search, - FileText, - User, - Clock + User } from 'lucide-react' import { queries } from '@renderer/lib/queries' import { useDebounce } from '@renderer/hooks/useDebounce' diff --git a/src/renderer/src/components/settings/BackupSettings.tsx b/src/renderer/src/components/settings/BackupSettings.tsx index 77aa6ae..6bafb8d 100644 --- a/src/renderer/src/components/settings/BackupSettings.tsx +++ b/src/renderer/src/components/settings/BackupSettings.tsx @@ -8,20 +8,18 @@ import { HardDrive, Download, RotateCcw, - Upload + Upload, + Database, + Zap, + History, + FileArchive } from 'lucide-react' import toast from 'react-hot-toast' import { Button } from '@renderer/components/ui/button' import { Input } from '@renderer/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' +import { Switch } from '@renderer/components/ui/switch' import { Label } from '@renderer/components/ui/label' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from '@renderer/components/ui/table' import { AlertDialog, AlertDialogAction, @@ -34,8 +32,9 @@ import { } from '@renderer/components/ui/alert-dialog' import { queries } from '@renderer/lib/queries' import { useSettings } from '@renderer/contexts/SettingsContext' -import { unwrapResult } from '@renderer/lib/utils' +import { unwrapResult, cn } from '@renderer/lib/utils' import { BackupInfo } from 'src/shared/types/backup' +import { Skeleton } from '@renderer/components/ui/skeleton' function formatBytes(bytes: number): string { if (bytes === 0) return '0 Bytes' @@ -117,7 +116,6 @@ export const BackupSettings = () => { toast.success('Database restored successfully. Restarting app...') setRestoreDialogOpen(false) setBackupToRestore(null) - // Wait a moment for the toast to be visible, then restart setTimeout(() => { window.electronApi.restartApp() }, 1000) @@ -144,7 +142,7 @@ export const BackupSettings = () => { try { const result = await window.electronApi.saveBackupAs(backup.path, backup.filename) if (result.canceled) { - // User cancelled, do nothing + // User cancelled } else if (result.success) { toast.success('Backup downloaded successfully') } else { @@ -210,54 +208,70 @@ export const BackupSettings = () => { } return ( -
    -

    Backup

    - - {/* Automatic Backups Section */} -
    -
    -

    Automatic Backups

    -

    - Configure automatic backup schedule and storage location -

    -
    - -
    -
    - + {/* Automatic Backups Card */} + + +
    +
    +
    + +
    +
    + Automatic Backups +

    + Configure backup schedule and storage location +

    +
    +
    + + + + {/* Enable Switch */} +
    + + setBackupEnabled(e.target.checked)} - className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" + onCheckedChange={setBackupEnabled} /> -
    -
    - + {/* Backup Folder */} +
    +
    +
    + +
    + +
    setBackupFolder(e.target.value)} placeholder="Default: ~/.opnotes/backups/" - className="flex-1" + className="flex-1 h-11" /> -
    -

    +

    Leave empty to use the default backup location

    + {/* Save Button and Last Backup */}
    -
    -
    - - {/* Quick Actions Section */} -
    -
    -

    Quick Actions

    -

    Create a backup or restore from a file

    -
    - -
    + + + + {/* Quick Actions Card */} + + +
    +
    + +
    + + Quick Actions + +
    +
    + +

    + Create a backup instantly or restore from a file +

    -
    -
    - - {/* Recent Backups Section */} -
    -
    -

    Recent Backups

    -

    - Up to 10 backups are kept. Older backups are automatically deleted. -

    -
    - -
    - - - - Filename - Date - Size - Actions - - - - {isLoadingBackups ? ( - - - Loading... - - - ) : !backups || backups.length === 0 ? ( - - - No backups found. Click "Backup Now" to create one. - - - ) : ( - backups.map((backup: BackupInfo) => ( - - {backup.filename} - {formatTimestamp(backup.timestamp)} - {formatBytes(backup.size)} - -
    - - - -
    -
    -
    - )) - )} -
    -
    -
    -
    + +
    + + {/* Recent Backups Card */} + + +
    +
    + +
    +
    + + Recent Backups + +

    + Up to 10 backups are kept. Older backups are automatically deleted. +

    +
    +
    +
    + + {isLoadingBackups ? ( +
    + {[1, 2, 3].map((i) => ( +
    + +
    + + +
    +
    + ))} +
    + ) : !backups || backups.length === 0 ? ( +
    +
    + +
    +

    No backups found

    +

    + Click "Backup Now" to create one +

    +
    + ) : ( +
    + {backups.map((backup: BackupInfo, index: number) => ( +
    +
    + +
    +
    +

    {backup.filename}

    +
    + {formatTimestamp(backup.timestamp)} + + {formatBytes(backup.size)} +
    +
    +
    + + + +
    +
    + ))} +
    + )} +
    +
    + {/* Delete Dialog */} @@ -401,6 +453,7 @@ export const BackupSettings = () => { + {/* Restore Dialog */} @@ -422,6 +475,7 @@ export const BackupSettings = () => { + {/* External Restore Dialog */} diff --git a/src/renderer/src/components/settings/GeneralSettings.tsx b/src/renderer/src/components/settings/GeneralSettings.tsx index dd96c2d..e7b04b8 100644 --- a/src/renderer/src/components/settings/GeneralSettings.tsx +++ b/src/renderer/src/components/settings/GeneralSettings.tsx @@ -9,7 +9,8 @@ import { } from '@renderer/components/ui/form' import { Input } from '@renderer/components/ui/input' import { Button } from '@renderer/components/ui/button' -import { Save, RotateCcw } from 'lucide-react' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' +import { Save, RotateCcw, Building2, Phone, Building, Wand2 } from 'lucide-react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { zodResolver } from '@hookform/resolvers/zod' @@ -82,86 +83,146 @@ export const GeneralSettings = () => { const submitForm = form.handleSubmit(onSubmit) return ( -
    -

    General

    -
    - ( - - Hospital - - - - - Name of the hospital, this will be used when printing BHT - - - - )} - /> - ( - - Unit - - - - - Name of the hospital unit, this will be used when printing BHT - - - - )} - /> - ( - - Telephone - - - - - Contact telephone number, this will be used when printing BHT - - - - )} - /> +
    + {/* Hospital Information Card */} + + +
    +
    +
    + +
    +
    + Hospital Information +

    + Details shown on printed reports +

    +
    +
    + -
    + + + + ( + +
    +
    + +
    + Hospital Name +
    + + + + + This will appear in the header of printed BHT reports + + +
    + )} + /> + +
    + ( + +
    +
    + +
    + Unit / Department +
    + + + + + Department or unit name + + +
    + )} + /> + + ( + +
    +
    + +
    + Contact Number +
    + + + + + Hospital contact number + + +
    + )} + /> +
    + +
    + +
    + + +
    + + + {/* Setup Wizard Card */} + + +
    +
    + +
    + + Setup Wizard + +
    +
    + +

    + Re-run the setup wizard to update your hospital information and review the app features. +

    -
    - - -
    -

    Setup Wizard

    -

    - Re-run the setup wizard to update your hospital information and review the app features. -

    - -
    + +
    ) } diff --git a/src/renderer/src/components/settings/TemplateForm.tsx b/src/renderer/src/components/settings/TemplateForm.tsx index a9a2082..4e833b9 100644 --- a/src/renderer/src/components/settings/TemplateForm.tsx +++ b/src/renderer/src/components/settings/TemplateForm.tsx @@ -1,9 +1,18 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, forwardRef, useImperativeHandle } from 'react' import { useForm, Controller } from 'react-hook-form' import { z } from 'zod' import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import toast from 'react-hot-toast' +import { + Type, + Layers, + Tag, + Globe, + User, + FileText, + Plus +} from 'lucide-react' import { Form, FormControl, @@ -15,6 +24,7 @@ import { } from '@renderer/components/ui/form' import { Input } from '@renderer/components/ui/input' import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { Select, SelectContent, @@ -24,9 +34,8 @@ import { } from '@renderer/components/ui/select' import { TagsInput } from '@renderer/components/ui/tags-input' import { RichTextEditor } from '@renderer/components/common/RichTextEditor' -import { Label } from '@renderer/components/ui/label' import { queries } from '@renderer/lib/queries' -import { unwrapResult } from '@renderer/lib/utils' +import { cn, unwrapResult } from '@renderer/lib/utils' import { SurgeryTemplate } from 'src/shared/types/db' // Helper function to parse tags from JSON string @@ -60,314 +69,438 @@ const formSchema = z.object({ type FormSchema = z.infer +export interface TemplateFormRef { + submit: () => void + isSubmitting: boolean +} + interface TemplateFormProps { template?: SurgeryTemplate | null onSuccess?: () => void - onCancel?: () => void } -export const TemplateForm = ({ template, onSuccess, onCancel }: TemplateFormProps) => { - const queryClient = useQueryClient() - const [customCategory, setCustomCategory] = useState('') - const [showCustomCategoryInput, setShowCustomCategoryInput] = useState(false) - const isEditing = !!template +export const TemplateForm = forwardRef( + ({ template, onSuccess }, ref) => { + const queryClient = useQueryClient() + const [customCategory, setCustomCategory] = useState('') + const [showCustomCategoryInput, setShowCustomCategoryInput] = useState(false) + const isEditing = !!template - const { data: doctorsData } = useQuery({ - ...queries.doctors.list({ pageSize: 100 }) - }) + const { data: doctorsData } = useQuery({ + ...queries.doctors.list({ pageSize: 100 }) + }) - const { data: existingCategories } = useQuery({ - queryKey: ['surgeryTemplates', 'categories'], - queryFn: () => unwrapResult(window.api.invoke('getTemplateCategories')) - }) + const { data: existingCategories } = useQuery({ + queryKey: ['surgeryTemplates', 'categories'], + queryFn: () => unwrapResult(window.api.invoke('getTemplateCategories')) + }) - // Merge predefined and existing categories - const allCategories = [ - ...new Set([...PREDEFINED_CATEGORIES, ...(existingCategories || [])]) - ].sort() + // Merge predefined and existing categories + const allCategories = [ + ...new Set([...PREDEFINED_CATEGORIES, ...(existingCategories || [])]) + ].sort() - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - title: '', - category: '', - tags: [], - scope: 'global', - doctorId: null, - content: '' - } - }) + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + category: '', + tags: [], + scope: 'global', + doctorId: null, + content: '' + } + }) - const scope = form.watch('scope') + const scope = form.watch('scope') - useEffect(() => { - if (template) { - form.reset({ - title: template.title, - category: template.category, - tags: parseTags(template.tags), - scope: template.doctor_id ? 'doctor' : 'global', - doctorId: template.doctor_id, - content: template.content - }) - } - }, [template, form]) + useEffect(() => { + if (template) { + form.reset({ + title: template.title, + category: template.category, + tags: parseTags(template.tags), + scope: template.doctor_id ? 'doctor' : 'global', + doctorId: template.doctor_id, + content: template.content + }) + } + }, [template, form]) - const createMutation = useMutation({ - mutationFn: async (data: FormSchema) => { - const payload = { - title: data.title, - category: data.category, - tags: JSON.stringify(data.tags), - doctor_id: data.scope === 'doctor' ? data.doctorId : null, - content: data.content + const createMutation = useMutation({ + mutationFn: async (data: FormSchema) => { + const payload = { + title: data.title, + category: data.category, + tags: JSON.stringify(data.tags), + doctor_id: data.scope === 'doctor' ? data.doctorId : null, + content: data.content + } + return unwrapResult(window.api.invoke('createSurgeryTemplate', payload)) + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['surgeryTemplates'] }) + toast.success('Template created successfully') + onSuccess?.() + }, + onError: (error: Error) => { + toast.error(error.message) } - return unwrapResult(window.api.invoke('createSurgeryTemplate', payload)) - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['surgeryTemplates'] }) - toast.success('Template created successfully') - onSuccess?.() - }, - onError: (error: Error) => { - toast.error(error.message) - } - }) + }) - const updateMutation = useMutation({ - mutationFn: async (data: FormSchema) => { - if (!template) return - const payload = { - title: data.title, - category: data.category, - tags: JSON.stringify(data.tags), - doctor_id: data.scope === 'doctor' ? data.doctorId : null, - content: data.content + const updateMutation = useMutation({ + mutationFn: async (data: FormSchema) => { + if (!template) return + const payload = { + title: data.title, + category: data.category, + tags: JSON.stringify(data.tags), + doctor_id: data.scope === 'doctor' ? data.doctorId : null, + content: data.content + } + return unwrapResult(window.api.invoke('updateSurgeryTemplateById', template.id, payload)) + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['surgeryTemplates'] }) + toast.success('Template updated successfully') + onSuccess?.() + }, + onError: (error: Error) => { + toast.error(error.message) } - return unwrapResult(window.api.invoke('updateSurgeryTemplateById', template.id, payload)) - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['surgeryTemplates'] }) - toast.success('Template updated successfully') - onSuccess?.() - }, - onError: (error: Error) => { - toast.error(error.message) - } - }) + }) - const onSubmit = async (data: FormSchema) => { - if (isEditing) { - await updateMutation.mutateAsync(data) - } else { - await createMutation.mutateAsync(data) + const onSubmit = async (data: FormSchema) => { + if (isEditing) { + await updateMutation.mutateAsync(data) + } else { + await createMutation.mutateAsync(data) + } } - } - const handleCategoryChange = (value: string) => { - if (value === '__custom__') { - setShowCustomCategoryInput(true) - return - } - form.setValue('category', value) - setCustomCategory('') - setShowCustomCategoryInput(false) - } + const isSubmitting = createMutation.isPending || updateMutation.isPending - const handleCustomCategorySubmit = () => { - if (customCategory.trim()) { - form.setValue('category', customCategory.trim()) - setShowCustomCategoryInput(false) + useImperativeHandle(ref, () => ({ + submit: () => form.handleSubmit(onSubmit)(), + isSubmitting + })) + + const handleCategoryChange = (value: string) => { + if (value === '__custom__') { + setShowCustomCategoryInput(true) + return + } + form.setValue('category', value) setCustomCategory('') + setShowCustomCategoryInput(false) } - } - return ( -
    - - ( - - Title * - - - - - - )} - /> + const handleCustomCategorySubmit = () => { + if (customCategory.trim()) { + form.setValue('category', customCategory.trim()) + setShowCustomCategoryInput(false) + setCustomCategory('') + } + } - ( - - Category * - - {showCustomCategoryInput && ( -
    - setCustomCategory(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - handleCustomCategorySubmit() - } - }} - autoFocus - /> - + return ( + + + {/* Template Details Card */} + + +
    +
    +
    +
    - )} - - - )} - /> +
    + Template Details +

    + Basic information about the template +

    +
    +
    + + + {/* Title */} + ( + +
    +
    + +
    + Title * +
    + + + + +
    + )} + /> - ( - - Tags - - + {/* Category */} + ( + +
    +
    + +
    + Category * +
    + + {showCustomCategoryInput && ( +
    + setCustomCategory(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleCustomCategorySubmit() + } + }} + className="h-10" + autoFocus + /> + +
    + )} + +
    + )} /> -
    - - Tags help organize and search templates (e.g., laparoscopic, emergency) - -
    - )} - /> - - Scope -
    -
    - { - form.setValue('scope', 'global') - form.setValue('doctorId', null) - }} - className="h-4 w-4" - /> - -
    -
    - form.setValue('scope', 'doctor')} - className="h-4 w-4" - /> - -
    -
    -
    + {/* Tags */} + ( + +
    +
    + +
    + Tags +
    + + + + + e.g., laparoscopic, emergency + +
    + )} + /> +
    - {scope === 'doctor' && ( - ( + {/* Scope */} - Doctor - - +
    +
    + +
    + Scope +
    +
    + + +
    - )} - /> - )} - ( - - Content * - - ( + +
    +
    + +
    + Doctor +
    + + +
    + )} /> -
    - -
    - )} - /> + )} + +
    -
    - {onCancel && ( - - )} - -
    - - - ) -} + {/* Content Card */} + + +
    +
    + +
    +
    + Template Content +

    + The text that will be inserted into surgery notes +

    +
    +
    +
    + + ( + + + + + + + )} + /> + +
    + + + ) + } +) + +TemplateForm.displayName = 'TemplateForm' diff --git a/src/renderer/src/components/settings/TemplatesSettings.tsx b/src/renderer/src/components/settings/TemplatesSettings.tsx index 67e55b8..e90995b 100644 --- a/src/renderer/src/components/settings/TemplatesSettings.tsx +++ b/src/renderer/src/components/settings/TemplatesSettings.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Plus, Search, Pencil, Trash2, Globe, User } from 'lucide-react' +import { Plus, Search, Pencil, Trash2, Globe, User, FileText, Layers } from 'lucide-react' import toast from 'react-hot-toast' import { Button } from '@renderer/components/ui/button' import { Input } from '@renderer/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { Select, SelectContent, @@ -12,14 +13,6 @@ import { SelectTrigger, SelectValue } from '@renderer/components/ui/select' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from '@renderer/components/ui/table' import { AlertDialog, AlertDialogAction, @@ -31,11 +24,11 @@ import { AlertDialogTitle } from '@renderer/components/ui/alert-dialog' import { Badge } from '@renderer/components/ui/badge' -import { unwrapResult } from '@renderer/lib/utils' +import { Skeleton } from '@renderer/components/ui/skeleton' +import { unwrapResult, cn } from '@renderer/lib/utils' import { SurgeryTemplate } from 'src/shared/types/db' import { queries } from '@renderer/lib/queries' -// Helper functions for plain objects (IPC serializes class instances to plain objects) const parseTags = (tags: string | null): string[] => { if (!tags) return [] try { @@ -57,7 +50,6 @@ export const TemplatesSettings = () => { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [templateToDelete, setTemplateToDelete] = useState(null) - // Debounce search const [debouncedSearch, setDebouncedSearch] = useState('') useMemo(() => { const timer = setTimeout(() => setDebouncedSearch(search), 300) @@ -130,121 +122,187 @@ export const TemplatesSettings = () => { } return ( -
    -
    -

    Templates

    - -
    +
    + {/* Header Card with Search */} + + +
    +
    +
    +
    + +
    +
    + Surgery Templates +

    + Pre-defined templates for surgery notes +

    +
    +
    + +
    + -
    -
    - - setSearch(e.target.value)} - className="pl-10" - /> -
    - -
    + + {/* Search and Filter Row */} +
    +
    + + setSearch(e.target.value)} + className="pl-10 h-11" + /> +
    + +
    +
    + -
    - - - - Title - Category - Tags - Scope - Actions - - - - {isLoading ? ( - - - Loading... - - - ) : templatesData?.data.length === 0 ? ( - - - No templates found. Click "Add Template" to create one. - - - ) : ( - templatesData?.data.map((template) => ( - - {template.title} - {template.category} - -
    - {parseTags(template.tags).map((tag) => ( - + {/* Templates List Card */} + + +
    +
    + +
    + + {templatesData?.total ?? 0} Template{(templatesData?.total ?? 0) !== 1 ? 's' : ''} + +
    +
    + + {isLoading ? ( +
    + {[1, 2, 3].map((i) => ( +
    + +
    + + +
    +
    + ))} +
    + ) : templatesData?.data.length === 0 ? ( +
    +
    + +
    +

    No templates found

    +

    + {search + ? `No results for "${search}". Try adjusting your search.` + : 'Create surgery templates to speed up documentation.'} +

    + {!search && ( + + )} +
    + ) : ( +
    + {templatesData?.data.map((template, index) => ( +
    +
    + +
    +
    +
    + {template.title} + + {template.category} + +
    +
    + {parseTags(template.tags).slice(0, 3).map((tag) => ( + {tag} ))} -
    - - - {isGlobal(template) ? ( -
    - - Global -
    - ) : ( -
    - - - {template.doctor_id - ? doctorsMap[template.doctor_id] || 'Doctor' - : 'Doctor'} + {parseTags(template.tags).length > 3 && ( + + +{parseTags(template.tags).length - 3} more + + )} + + {isGlobal(template) ? ( + + + Global + + ) : ( + + + {template.doctor_id ? doctorsMap[template.doctor_id] || 'Doctor' : 'Doctor'} -
    - )} -
    - -
    - - + )}
    -
    - - )) - )} - -
    -
    +
    +
    + + +
    +
    + ))} +
    + )} + + + {/* Delete Dialog */} diff --git a/src/renderer/src/components/ui/switch.tsx b/src/renderer/src/components/ui/switch.tsx new file mode 100644 index 0000000..6d839b8 --- /dev/null +++ b/src/renderer/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@renderer/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/renderer/src/routes/doctors/add-doctor.tsx b/src/renderer/src/routes/doctors/add-doctor.tsx index df3340e..43210b3 100644 --- a/src/renderer/src/routes/doctors/add-doctor.tsx +++ b/src/renderer/src/routes/doctors/add-doctor.tsx @@ -29,7 +29,7 @@ export const AddNewDoctor = () => { }) useEffect(() => { - setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }, { label: 'Add Doctor' }]) + setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }]) }, [setBreadcrumbs]) return ( diff --git a/src/renderer/src/routes/doctors/edit-doctor.tsx b/src/renderer/src/routes/doctors/edit-doctor.tsx index d8dcd4f..a847c4b 100644 --- a/src/renderer/src/routes/doctors/edit-doctor.tsx +++ b/src/renderer/src/routes/doctors/edit-doctor.tsx @@ -34,8 +34,8 @@ export const EditDoctor = () => { }) useEffect(() => { - setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }, { label: data?.name || 'Doctor' }]) - }, [setBreadcrumbs, data?.name]) + setBreadcrumbs([{ label: 'Doctors', to: '/doctors' }]) + }, [setBreadcrumbs]) return (
    diff --git a/src/renderer/src/routes/patients/add-new-patient.tsx b/src/renderer/src/routes/patients/add-new-patient.tsx index bd11a1e..4df378d 100644 --- a/src/renderer/src/routes/patients/add-new-patient.tsx +++ b/src/renderer/src/routes/patients/add-new-patient.tsx @@ -1,19 +1,19 @@ -import { NewPatientForm } from '@renderer/components/patient/NewPatientForm' +import { NewPatientForm, NewPatientFormRef } from '@renderer/components/patient/NewPatientForm' +import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' +import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' +import { Save, UserPlus, Hash, Cake, Building2, Lightbulb } from 'lucide-react' import { useEffect, useRef } from 'react' import toast from 'react-hot-toast' import { useNavigate } from 'react-router-dom' import { Patient } from 'src/shared/types/db' -import { NewPatientFormRef } from '../../components/patient/NewPatientForm' -import { Button } from '@renderer/components/ui/button' -import { RotateCcw, SaveIcon } from 'lucide-react' -import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' export const AddNewPatient = () => { const navigate = useNavigate() const ref = useRef(null) const { setBreadcrumbs } = useBreadcrumbs() + const handleNewPatient = (patient: Patient) => { toast.success('Patient added successfully') navigate(`/patients/${patient.id}`) @@ -27,34 +27,108 @@ export const AddNewPatient = () => { } }) - const actions = ( - <> - - - - ) - useEffect(() => { - setBreadcrumbs([{ label: 'Patients', to: '/patients' }, { label: 'Add Patient' }]) + setBreadcrumbs([{ label: 'Patients', to: '/patients' }]) }, [setBreadcrumbs]) return ( - - - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Add Patient

    +

    + Register a new patient to the system +

    +
    +
    + +
    + + {/* Main Content - Two Column Layout */} +
    +
    + {/* Left: Form */} +
    + +
    + + {/* Right: Tips Sidebar */} +
    + {/* Tips Card */} + + +
    +
    + +
    + + Quick Tips + +
    +
    + +
    +
    + +
    +
    +

    PHN (Patient Hospital Number)

    +

    + The unique identifier assigned by the hospital +

    +
    +
    + +
    +
    + +
    +
    +

    Date of Birth

    +

    + Age will be calculated automatically from DOB +

    +
    +
    + +
    +
    + +
    +
    +

    Ward

    +

    + Current ward assignment (can be updated later) +

    +
    +
    +
    +
    + + {/* Usage Info */} + + +

    + Patients can have multiple surgeries associated with them. After adding a patient, you can create surgery records for their procedures. +

    +
    +
    +
    +
    +
    +
    ) } diff --git a/src/renderer/src/routes/patients/edit-patient.tsx b/src/renderer/src/routes/patients/edit-patient.tsx index 84486c0..9c1c500 100644 --- a/src/renderer/src/routes/patients/edit-patient.tsx +++ b/src/renderer/src/routes/patients/edit-patient.tsx @@ -1,15 +1,19 @@ import { NewPatientForm, NewPatientFormRef } from '@renderer/components/patient/NewPatientForm' +import { Button } from '@renderer/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' +import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' +import { queries } from '@renderer/lib/queries' +import { cn } from '@renderer/lib/utils' import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query' -import { useEffect, useMemo, useRef } from 'react' +import { format } from 'date-fns' +import { Save, UserCog, Hash, Cake, User, Building2, Calendar, Eye } from 'lucide-react' +import { useEffect, useRef } from 'react' import toast from 'react-hot-toast' import { useParams } from 'react-router-dom' import { Patient } from 'src/shared/types/db' -import { queries } from '../../lib/queries' -import { AppLayout } from '@renderer/layouts/AppLayout' -import { Button } from '@renderer/components/ui/button' -import { RotateCcw, SaveIcon } from 'lucide-react' -import { useKeyboardEvent } from '@renderer/hooks/useKeyboardEvent' +import womenIcon from '../../../../../resources/woman.png?asset' +import manIcon from '../../../../../resources/man.png?asset' export const getPatientByIdQuery = (id: number) => queryOptions({ @@ -38,45 +42,165 @@ export const EditPatient = () => { } }) - const actions = ( - <> - - - - ) - - const ptName = useMemo(() => data?.name || data?.phn || 'Patient', [data?.name, data?.phn]) - useEffect(() => { - setBreadcrumbs([ - { label: 'Patients', to: '/patients' }, - { - label: ptName, - to: `/patients/${id}` - }, - { label: 'Edit' } - ]) - }, [ptName, id, setBreadcrumbs]) + setBreadcrumbs([{ label: 'Patients', to: '/patients' }]) + }, [setBreadcrumbs]) return ( - - {!isLoading && data && ( - - )} - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Edit Patient

    +

    + Update details for {data?.name || data?.phn || 'patient'} +

    +
    +
    + +
    + + {/* Main Content - Two Column Layout */} +
    +
    + {/* Left: Form */} +
    + {!isLoading && data && ( + + )} +
    + + {/* Right: Patient Preview Sidebar */} +
    + {/* Current Info Preview */} + + +
    +
    +
    + +
    + + Current Info + +
    + + + {data && ( + <> + {/* Patient Avatar and Name */} +
    +
    +
    + Patient +
    +
    +
    +

    {data.name || 'Unknown'}

    +

    {data.phn}

    +
    +
    + + {/* Details */} +
    +
    +
    + +
    +
    +

    PHN

    +

    + {data.phn} +

    +
    +
    + +
    +
    + +
    +
    +

    Age

    +

    + {data.age > 1 ? data.age : '<1'} years +

    +
    +
    + +
    +
    + +
    +
    +

    Gender

    +

    + {data.gender === 'M' ? 'Male' : 'Female'} +

    +
    +
    + +
    +
    + +
    +
    +

    Ward

    +

    + {data.ward || 'Not assigned'} +

    +
    +
    + +
    +
    + +
    +
    +

    Added

    +

    + {data.created_at ? format(new Date(data.created_at), 'MMM d, yyyy') : 'Unknown'} +

    +
    +
    +
    + + )} +
    + + + {/* Usage Info */} + + +

    + Changes will be reflected in all surgeries associated with this patient. +

    +
    +
    +
    +
    +
    +
    ) } diff --git a/src/renderer/src/routes/patients/list-patients.tsx b/src/renderer/src/routes/patients/list-patients.tsx index 7e99be0..c508bb4 100644 --- a/src/renderer/src/routes/patients/list-patients.tsx +++ b/src/renderer/src/routes/patients/list-patients.tsx @@ -1,12 +1,5 @@ import { Button } from '@renderer/components/ui/button' import { Card, CardContent } from '@renderer/components/ui/card' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger -} from '@renderer/components/ui/dropdown-menu' import { Input } from '@renderer/components/ui/input' import { Select, @@ -26,13 +19,11 @@ import { ChevronLeft, ChevronRight, Copy, - Edit3, - Eye, FileSearch, - MoreVertical, + Pencil, Plus, - Scissors, Search, + Stethoscope, Users } from 'lucide-react' import { useEffect, useState } from 'react' @@ -139,54 +130,46 @@ function PatientRow({
    {patient.updated_at.toLocaleDateString()}
    - - e.stopPropagation()}> - - - - { - e.stopPropagation() - navigator.clipboard.writeText(patient.phn) - toast.success('PHN copied to clipboard') - }} - > - - Copy PHN - - - { - e.stopPropagation() - onNavigate(`/patients/${patient.id}`) - }} - > - - View Patient - - { - e.stopPropagation() - onNavigate(`/patients/${patient.id}/edit`) - }} - > - - Edit Patient - - - { - e.stopPropagation() - onNavigate(`/patients/${patient.id}/surgeries/add`) - }} - > - - Add Surgery - - - + {/* Quick Action Buttons */} +
    + + + +
    ) } diff --git a/src/renderer/src/routes/patients/view-patient.tsx b/src/renderer/src/routes/patients/view-patient.tsx index 852b72c..1ece6a1 100644 --- a/src/renderer/src/routes/patients/view-patient.tsx +++ b/src/renderer/src/routes/patients/view-patient.tsx @@ -1,6 +1,5 @@ import { Button } from '@renderer/components/ui/button' import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { QueryClient, queryOptions, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { EditIcon, @@ -21,7 +20,7 @@ import { Building2, Cake } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import { Link, LoaderFunctionArgs, useLoaderData, useNavigate } from 'react-router-dom' import { PatientModel } from 'src/shared/models/PatientModel' import { getPatientByIdQuery } from './edit-patient' @@ -440,31 +439,51 @@ export const ViewPatient = () => { const { id } = useLoaderData() as Awaited>> const { data: patient } = useSuspenseQuery(getPatientByIdQuery(parseInt(id))) - const ptName = useMemo( - () => patient?.name || patient?.phn || 'Patient', - [patient?.name, patient?.phn] - ) - useEffect(() => { - setBreadcrumbs([{ label: 'Patients', to: '/patients' }, { label: ptName }]) - }, [ptName, setBreadcrumbs]) + setBreadcrumbs([{ label: 'Patients', to: '/patients' }]) + }, [setBreadcrumbs]) const handleAddSurgery = () => navigate(`/patients/${id}/surgeries/add`) const handleEdit = () => navigate(`/patients/${id}/edit`) return ( - -
    - {/* Left Sidebar - Patient Info */} -
    - +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    {patient?.name || 'Patient'}

    +

    + {patient?.phn} +

    +
    + +
    + + {/* Main Content */} +
    +
    + {/* Left Sidebar - Patient Info */} +
    + +
    - {/* Main Content - Surgeries */} -
    - + {/* Main Content - Surgeries */} +
    + +
    - +
    ) } diff --git a/src/renderer/src/routes/settings/settings-index.tsx b/src/renderer/src/routes/settings/settings-index.tsx index cdbd75a..9ecaef1 100644 --- a/src/renderer/src/routes/settings/settings-index.tsx +++ b/src/renderer/src/routes/settings/settings-index.tsx @@ -1,8 +1,7 @@ import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' -import { AppLayout } from '@renderer/layouts/AppLayout' import { useEffect } from 'react' import { useSearchParams } from 'react-router-dom' -import { Settings, FileText, HardDrive } from 'lucide-react' +import { Settings, FileText, HardDrive, Building2 } from 'lucide-react' import { cn } from '@renderer/lib/utils' import { GeneralSettings } from '@renderer/components/settings/GeneralSettings' import { TemplatesSettings } from '@renderer/components/settings/TemplatesSettings' @@ -14,22 +13,41 @@ type SettingsSection = 'general' | 'templates' | 'backup' interface NavItemProps { icon: React.ReactNode label: string + description: string isActive: boolean onClick: () => void + color: string } -const NavItem = ({ icon, label, isActive, onClick }: NavItemProps) => ( +const NavItem = ({ icon, label, description, isActive, onClick, color }: NavItemProps) => ( ) @@ -50,32 +68,57 @@ export const SettingsIndex = () => { }, [setBreadcrumbs]) return ( - +
    + {/* Header Section */} +
    +
    + +
    +
    +

    Settings

    +

    + Manage application preferences and configurations +

    +
    +
    + {/* Desktop layout with sidebar */} -
    -
    ) } diff --git a/src/renderer/src/components/onboarding/steps/HospitalInfoStep.tsx b/src/renderer/src/components/onboarding/steps/HospitalInfoStep.tsx index e041e50..9baf33c 100644 --- a/src/renderer/src/components/onboarding/steps/HospitalInfoStep.tsx +++ b/src/renderer/src/components/onboarding/steps/HospitalInfoStep.tsx @@ -15,7 +15,7 @@ import { useSettings } from '@renderer/contexts/SettingsContext' import { useEffect, useImperativeHandle, forwardRef } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { queries } from '@renderer/lib/queries' -import { Building2 } from 'lucide-react' +import { Building2, Phone, MapPin } from 'lucide-react' const formSchema = z.object({ hospital: z.string().min(1, 'Hospital name is required'), @@ -78,65 +78,108 @@ export const HospitalInfoStep = forwardRef((_, ref) => { })) return ( -
    -
    - -
    -
    -

    Hospital Information

    -

    - Enter your hospital details. This information will appear on printed documents. -

    +
    + {/* Header */} +
    +
    +
    +
    + +
    +
    +
    +

    Hospital Information

    +

    + This information will appear on printed surgical notes and reports +

    +
    + {/* Form */}
    - + ( - - Hospital Name + + + + Hospital Name + - + - - The name of your hospital (e.g., General Hospital Colombo) + + The official name of your healthcare facility )} /> + ( - - Unit Name + + + + Unit / Department + - + - Your department or unit (e.g., Surgical Unit A) + + Your department or ward within the hospital + )} /> + ( - - Contact Telephone + + + + Contact Number + (optional) + - + - Contact number for the unit (optional) + + Unit contact number for printed documents + )} /> + + {/* Info note */} +
    +

    + You can update these settings anytime from{' '} + Settings → General +

    +
    ) }) diff --git a/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx b/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx index c936e4b..cc8e05b 100644 --- a/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx +++ b/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx @@ -1,85 +1,110 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from '@renderer/components/ui/card' -import { Search, Printer, Users, CalendarCheck, Sparkles } from 'lucide-react' +import { Sparkles, Search, Printer, Users, CalendarCheck, Keyboard, Command } from 'lucide-react' + +const tips = [ + { + icon: Search, + title: 'Powerful Search', + description: 'Find patients and surgeries instantly by name, BHT, diagnosis, or procedure', + shortcut: '⌘ + K' + }, + { + icon: Printer, + title: 'Print Documents', + description: 'Generate professional surgical notes and reports with one click', + shortcut: '⌘ + P' + }, + { + icon: Users, + title: 'Doctor Database', + description: 'Maintain a complete list of surgeons and assistants for quick assignment', + shortcut: null + }, + { + icon: CalendarCheck, + title: 'Follow-up Tracking', + description: 'Record post-operative visits and track patient recovery progress', + shortcut: null + } +] export const QuickTipsStep = () => { return ( -
    -
    - -
    -
    -

    Quick Tips

    -

    - Here are some helpful features to get you started. -

    +
    + {/* Header */} +
    +
    +
    +
    + +
    +
    +
    +

    Quick Tips

    +

    + Here are some powerful features to help you get the most out of Op Notes +

    +
    -
    - - - - - Powerful Search - - - - - Quickly find patients and surgeries using full-text search. Search by name, BHT - number, diagnosis, or procedure. - - - - - - - - - Print Documents - - - - - Print surgery notes, patient records, and follow-up summaries. Your hospital details - are automatically included. - - - + {/* Tips grid */} +
    + {tips.map((tip, index) => { + const Icon = tip.icon + return ( +
    +
    +
    + +
    + {tip.shortcut && ( +
    + + {tip.shortcut} +
    + )} +
    +

    {tip.title}

    +

    + {tip.description} +

    +
    + ) + })} +
    - - - - - Doctor Management - - - - - Maintain a database of doctors. Assign surgeons and assistants to each surgery for - complete records. - - - + {/* Keyboard shortcuts hint */} +
    +
    +
    +
    + +
    + + +
    + K +
    +
    + + Open Command Palette for quick actions + +
    +
    - - - - - Follow-up Tracking - - - - - Record follow-up visits and track patient progress after surgery. Never lose track of - post-operative care. - - - + {/* Ready message */} +
    +

    + You're all set! 🎉 +

    +

    + Click “Get Started” to begin using Op Notes +

    ) diff --git a/src/renderer/src/components/onboarding/steps/TemplatesStep.tsx b/src/renderer/src/components/onboarding/steps/TemplatesStep.tsx index be7544b..9209cd0 100644 --- a/src/renderer/src/components/onboarding/steps/TemplatesStep.tsx +++ b/src/renderer/src/components/onboarding/steps/TemplatesStep.tsx @@ -1,59 +1,96 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from '@renderer/components/ui/card' -import { FileText, Copy, Clock } from 'lucide-react' +import { FileText, Zap, Clock, Copy } from 'lucide-react' + +const benefits = [ + { + icon: Copy, + title: 'Create Once, Use Many Times', + description: 'Define standard procedures and post-op instructions once, then apply them instantly to new surgeries', + color: 'text-blue-500', + bgColor: 'bg-blue-500/10' + }, + { + icon: Clock, + title: 'Save Hours Every Week', + description: 'Stop retyping the same information. Templates auto-fill surgery notes so you can focus on patient-specific details', + color: 'text-emerald-500', + bgColor: 'bg-emerald-500/10' + }, + { + icon: Zap, + title: 'Consistent Documentation', + description: 'Ensure all surgical notes follow your unit\'s standards with pre-defined templates', + color: 'text-amber-500', + bgColor: 'bg-amber-500/10' + } +] export const TemplatesStep = () => { return ( -
    -
    - -
    -
    -

    Surgery Templates

    -

    - Save time by creating reusable templates for common surgical procedures. -

    +
    + {/* Header */} +
    +
    +
    +
    + +
    +
    +
    +

    Surgery Templates

    +

    + Save time with reusable templates for common surgical procedures +

    +
    -
    - - - - - Create Once, Use Many Times - - - - - Define standard procedures, pre-op preparations, and post-op instructions once. Apply - them to new surgeries with a single click. - - - + {/* Benefits list */} +
    + {benefits.map((benefit, index) => { + const Icon = benefit.icon + return ( +
    +
    + +
    +
    +

    {benefit.title}

    +

    + {benefit.description} +

    +
    +
    + ) + })} +
    - - - - - Reduce Documentation Time - - - - - Stop retyping the same information. Templates auto-fill surgery notes, letting you - focus on patient-specific details. - - - + {/* Example preview */} +
    +
    +
    + +
    + Example Template +
    +
    +
    +
    +
    +
    +
    -

    - You can create and manage templates from Settings → Templates after completing setup. + {/* Info note */} +

    + Create and manage templates from{' '} + Settings → Templates{' '} + after completing setup

    ) diff --git a/src/renderer/src/components/onboarding/steps/WelcomeStep.tsx b/src/renderer/src/components/onboarding/steps/WelcomeStep.tsx index c4a67ae..326288d 100644 --- a/src/renderer/src/components/onboarding/steps/WelcomeStep.tsx +++ b/src/renderer/src/components/onboarding/steps/WelcomeStep.tsx @@ -1,17 +1,86 @@ -import hero from '@renderer/assets/hero.svg?asset' +import opNotesLogo from '@renderer/assets/opnotes-logo.png' +import { Stethoscope, Users, FileText, Shield } from 'lucide-react' + +const features = [ + { + icon: Stethoscope, + title: 'Surgery Management', + description: 'Complete surgical records with procedures, findings, and post-op notes' + }, + { + icon: Users, + title: 'Patient Records', + description: 'Comprehensive patient profiles with full medical history' + }, + { + icon: FileText, + title: 'Smart Templates', + description: 'Reusable templates for common procedures to save time' + }, + { + icon: Shield, + title: 'Secure & Local', + description: 'All data stored locally on your device for privacy' + } +] export const WelcomeStep = () => { return ( -
    - Op Notes -

    Welcome to Op Notes

    -

    - A surgical notes management application designed to help you efficiently manage patient - records, surgery notes, and follow-up care. -

    -
    - Let's get started by setting up your hospital information. +
    + {/* Hero section */} +
    + {/* Logo with glow effect */} +
    +
    +
    + Op Notes +
    +
    + + {/* Title */} +
    +

    + Welcome to{' '} + + Op Notes + +

    +

    + A surgical notes management system designed for healthcare professionals +

    +
    + + {/* Feature grid */} +
    + {features.map((feature, index) => { + const Icon = feature.icon + return ( +
    +
    + +
    +

    {feature.title}

    +

    + {feature.description} +

    +
    + ) + })} +
    + + {/* CTA hint */} +

    + Let's set up your workspace in just a few steps +

    ) } From b24eb3d7a58cbebf48920258335e6de0488127ca Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 03:10:46 +0530 Subject: [PATCH 19/26] feat: enhance retro theme with CRT effects and improved colors - Increase base font size to 20px for VT323 pixel font readability - Add richer amber/green phosphor colors for authentic CRT feel - Add scanline overlay and vignette effects in dark mode - Add phosphor glow effects on text, buttons, and inputs - Custom retro-styled scrollbars with green glow - Warmer sepia tones for light mode (aged paper aesthetic) - Hard-edged shadows for pixelated retro feel --- .../src/components/theme/ThemeDropdown.tsx | 98 +++-- src/renderer/src/styles/themes.css | 352 +++++++++++++----- 2 files changed, 332 insertions(+), 118 deletions(-) diff --git a/src/renderer/src/components/theme/ThemeDropdown.tsx b/src/renderer/src/components/theme/ThemeDropdown.tsx index 42f7c4d..04ad87b 100644 --- a/src/renderer/src/components/theme/ThemeDropdown.tsx +++ b/src/renderer/src/components/theme/ThemeDropdown.tsx @@ -1,6 +1,6 @@ import { useTheme, type ThemeId, type ThemeBase } from '@renderer/contexts/ThemeContext' import { cn } from '@renderer/lib/utils' -import { Check, Sparkles } from 'lucide-react' +import { Check } from 'lucide-react' import { Button } from '@renderer/components/ui/button' import { Popover, @@ -8,6 +8,12 @@ import { PopoverTrigger } from '@renderer/components/ui/popover' import { useState } from 'react' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@renderer/components/ui/tooltip' // Theme visual data with gradients and preview colors const themeVisuals: Record< @@ -182,35 +188,67 @@ export function ThemeDropdown() { return ( - - - + + + + + + + + +

    Change theme

    +
    +
    +
    Date: Mon, 19 Jan 2026 03:21:48 +0530 Subject: [PATCH 20/26] feat: add ember, frost, and sakura themes New themes: - Ember: Volcanic fire aesthetic with crimson/orange on charred black - Frost: Arctic ice theme with crystalline blues and sharp edges - Sakura: Japanese cherry blossom elegance with soft pinks and zen grays Also: - Set ocean-dark as default splash screen theme - Update theme picker grid to 4 columns for 12 themes --- src/renderer/index.html | 2 +- .../src/components/theme/ThemeDropdown.tsx | 30 +- .../src/components/theme/ThemeSwitcher.tsx | 21 +- src/renderer/src/contexts/ThemeContext.tsx | 61 ++- src/renderer/src/index.css | 2 +- src/renderer/src/styles/themes.css | 405 ++++++++++++++++++ 6 files changed, 508 insertions(+), 13 deletions(-) diff --git a/src/renderer/index.html b/src/renderer/index.html index 66425df..e045712 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/renderer/src/components/theme/ThemeDropdown.tsx b/src/renderer/src/components/theme/ThemeDropdown.tsx index 04ad87b..43b76c9 100644 --- a/src/renderer/src/components/theme/ThemeDropdown.tsx +++ b/src/renderer/src/components/theme/ThemeDropdown.tsx @@ -68,6 +68,21 @@ const themeVisuals: Record< gradient: 'from-amber-400 via-yellow-500 to-green-500', preview: { bg: '#1a1408', accent: '#4aff4a', text: '#ffcc00' }, icon: '📺' + }, + ember: { + gradient: 'from-red-500 via-orange-500 to-amber-500', + preview: { bg: '#1a0a08', accent: '#ff6a35', text: '#ffd0b0' }, + icon: '🔥' + }, + frost: { + gradient: 'from-cyan-300 via-blue-400 to-indigo-400', + preview: { bg: '#0a1420', accent: '#6bb8e8', text: '#e0f0ff' }, + icon: '❄️' + }, + sakura: { + gradient: 'from-pink-300 via-rose-400 to-fuchsia-400', + preview: { bg: '#1a0a14', accent: '#e890b0', text: '#ffe8f0' }, + icon: '🌸' } } @@ -80,7 +95,10 @@ const themeLabels: Record = { midnight: { name: 'Midnight', tagline: 'Purple elegance' }, obsidian: { name: 'Obsidian', tagline: 'True black AMOLED' }, ocean: { name: 'Ocean', tagline: 'Calm & professional' }, - retro: { name: 'Retro', tagline: 'CRT nostalgia' } + retro: { name: 'Retro', tagline: 'CRT nostalgia' }, + ember: { name: 'Ember', tagline: 'Volcanic fire' }, + frost: { name: 'Frost', tagline: 'Arctic ice' }, + sakura: { name: 'Sakura', tagline: 'Cherry blossom' } } interface ThemeCardProps { @@ -183,8 +201,8 @@ export function ThemeDropdown() { const isDark = theme.mode === 'dark' const currentVisuals = themeVisuals[theme.base] - // All 9 themes - const allThemes: ThemeBase[] = ['aurora', 'meadow', 'bloom', 'latte', 'slate', 'midnight', 'obsidian', 'ocean', 'retro'] + // All 12 themes + const allThemes: ThemeBase[] = ['aurora', 'meadow', 'bloom', 'latte', 'slate', 'midnight', 'obsidian', 'ocean', 'retro', 'ember', 'frost', 'sakura'] return ( @@ -251,7 +269,7 @@ export function ThemeDropdown() { {/* Header */} @@ -297,8 +315,8 @@ export function ThemeDropdown() {
    - {/* Theme grid - 3x3 for 9 themes */} -
    + {/* Theme grid - 4x3 for 12 themes */} +
    {allThemes.map((base) => { const isCurrentBase = theme.base === base return ( diff --git a/src/renderer/src/components/theme/ThemeSwitcher.tsx b/src/renderer/src/components/theme/ThemeSwitcher.tsx index 0b6b14c..c4f5ed0 100644 --- a/src/renderer/src/components/theme/ThemeSwitcher.tsx +++ b/src/renderer/src/components/theme/ThemeSwitcher.tsx @@ -43,6 +43,18 @@ const themePreviewColors: Record diff --git a/src/renderer/src/contexts/ThemeContext.tsx b/src/renderer/src/contexts/ThemeContext.tsx index 14beb51..1bb3db1 100644 --- a/src/renderer/src/contexts/ThemeContext.tsx +++ b/src/renderer/src/contexts/ThemeContext.tsx @@ -19,8 +19,14 @@ export type ThemeId = | 'ocean-dark' | 'retro-light' | 'retro-dark' + | 'ember-light' + | 'ember-dark' + | 'frost-light' + | 'frost-dark' + | 'sakura-light' + | 'sakura-dark' -export type ThemeBase = 'aurora' | 'meadow' | 'bloom' | 'latte' | 'slate' | 'midnight' | 'obsidian' | 'ocean' | 'retro' +export type ThemeBase = 'aurora' | 'meadow' | 'bloom' | 'latte' | 'slate' | 'midnight' | 'obsidian' | 'ocean' | 'retro' | 'ember' | 'frost' | 'sakura' export type ThemeMode = 'light' | 'dark' export interface Theme { @@ -176,6 +182,54 @@ export const themes: Theme[] = [ mode: 'dark', fontFamily: 'VT323', description: 'CRT phosphor glow, amber on black' + }, + { + id: 'ember-light', + name: 'Ember', + base: 'ember', + mode: 'light', + fontFamily: 'Nunito', + description: 'Warm crimson, volcanic energy' + }, + { + id: 'ember-dark', + name: 'Ember', + base: 'ember', + mode: 'dark', + fontFamily: 'Nunito', + description: 'Molten fire on charred black' + }, + { + id: 'frost-light', + name: 'Frost', + base: 'frost', + mode: 'light', + fontFamily: 'Inter', + description: 'Crystalline ice, sharp and pure' + }, + { + id: 'frost-dark', + name: 'Frost', + base: 'frost', + mode: 'dark', + fontFamily: 'Inter', + description: 'Frozen depths, arctic glow' + }, + { + id: 'sakura-light', + name: 'Sakura', + base: 'sakura', + mode: 'light', + fontFamily: 'Source Sans 3', + description: 'Cherry blossom elegance' + }, + { + id: 'sakura-dark', + name: 'Sakura', + base: 'sakura', + mode: 'dark', + fontFamily: 'Source Sans 3', + description: 'Twilight petals, zen serenity' } ] @@ -321,6 +375,9 @@ export function getThemesByBase(): Record { midnight: themes.filter((t) => t.base === 'midnight'), obsidian: themes.filter((t) => t.base === 'obsidian'), ocean: themes.filter((t) => t.base === 'ocean'), - retro: themes.filter((t) => t.base === 'retro') + retro: themes.filter((t) => t.base === 'retro'), + ember: themes.filter((t) => t.base === 'ember'), + frost: themes.filter((t) => t.base === 'frost'), + sakura: themes.filter((t) => t.base === 'sakura') } } diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index 55c4b34..5e2da14 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -1,7 +1,7 @@ @import 'tailwindcss'; /* Custom dark mode variant for data-theme attribute - matches all dark theme variants */ -@custom-variant dark (&:where([data-theme=aurora-dark], [data-theme=meadow-dark], [data-theme=bloom-dark], [data-theme=latte-dark], [data-theme=slate-dark], [data-theme=midnight-dark], [data-theme=obsidian-dark], [data-theme=ocean-dark], [data-theme=retro-dark], [data-theme=aurora-dark] *, [data-theme=meadow-dark] *, [data-theme=bloom-dark] *, [data-theme=latte-dark] *, [data-theme=slate-dark] *, [data-theme=midnight-dark] *, [data-theme=obsidian-dark] *, [data-theme=ocean-dark] *, [data-theme=retro-dark] *)); +@custom-variant dark (&:where([data-theme=aurora-dark], [data-theme=meadow-dark], [data-theme=bloom-dark], [data-theme=latte-dark], [data-theme=slate-dark], [data-theme=midnight-dark], [data-theme=obsidian-dark], [data-theme=ocean-dark], [data-theme=retro-dark], [data-theme=ember-dark], [data-theme=frost-dark], [data-theme=sakura-dark], [data-theme=aurora-dark] *, [data-theme=meadow-dark] *, [data-theme=bloom-dark] *, [data-theme=latte-dark] *, [data-theme=slate-dark] *, [data-theme=midnight-dark] *, [data-theme=obsidian-dark] *, [data-theme=ocean-dark] *, [data-theme=retro-dark] *, [data-theme=ember-dark] *, [data-theme=frost-dark] *, [data-theme=sakura-dark] *)); /* Source paths for class detection - relative to this CSS file */ @source "."; diff --git a/src/renderer/src/styles/themes.css b/src/renderer/src/styles/themes.css index c0c8a6d..404bf1a 100644 --- a/src/renderer/src/styles/themes.css +++ b/src/renderer/src/styles/themes.css @@ -1123,6 +1123,411 @@ --chart-5: oklch(65% 0.14 300); } +/* ============================================ + EMBER - Volcanic Fire & Molten Energy + Deep crimson with orange flames on charred black + Font: Nunito | Radius: Sharp | Borders: Bold + + Design: Intense, dramatic, powerful. Like looking into + the heart of a volcano with molten lava flows. + ============================================ */ + +[data-theme='ember-light'] { + --font-family: var(--font-nunito); + --radius-base: 0.375rem; + --border-width: 1px; + + /* Warm cream with subtle red undertone */ + --background: oklch(97% 0.012 25); + --foreground: oklch(20% 0.04 25); + + /* Parchment cards with warmth */ + --card: oklch(99% 0.008 25); + --card-foreground: oklch(20% 0.04 25); + + --popover: oklch(99% 0.008 25); + --popover-foreground: oklch(20% 0.04 25); + + /* Vibrant crimson red primary */ + --primary: oklch(52% 0.2 25); + --primary-foreground: oklch(98% 0.01 25); + + /* Warm beige secondary */ + --secondary: oklch(94% 0.02 50); + --secondary-foreground: oklch(25% 0.04 25); + + --muted: oklch(95% 0.015 40); + --muted-foreground: oklch(45% 0.03 25); + + /* Orange accent */ + --accent: oklch(92% 0.025 50); + --accent-foreground: oklch(25% 0.04 25); + + --destructive: oklch(50% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(88% 0.02 30); + --input: oklch(88% 0.02 30); + --ring: oklch(52% 0.2 25); + + --sidebar-background: oklch(95% 0.015 30); + --sidebar-foreground: oklch(30% 0.04 25); + --sidebar-primary: oklch(52% 0.2 25); + --sidebar-primary-foreground: oklch(98% 0.01 25); + --sidebar-accent: oklch(91% 0.02 40); + --sidebar-accent-foreground: oklch(25% 0.04 25); + --sidebar-border: oklch(88% 0.02 30); + --sidebar-ring: oklch(52% 0.2 25); + + --gradient-primary: linear-gradient(135deg, oklch(52% 0.2 25), oklch(58% 0.18 40)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(92% 0.03 30 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.4) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(20% 0.04 25 / 0.08); + --shadow-md: 0 4px 6px -1px oklch(20% 0.04 25 / 0.1), 0 2px 4px -2px oklch(20% 0.04 25 / 0.06); + --shadow-lg: 0 10px 15px -3px oklch(20% 0.04 25 / 0.12), 0 4px 6px -4px oklch(20% 0.04 25 / 0.08); + --shadow-primary: 0 4px 14px -3px oklch(52% 0.2 25 / 0.4); + + --chart-1: oklch(52% 0.2 25); + --chart-2: oklch(65% 0.18 45); + --chart-3: oklch(55% 0.12 200); + --chart-4: oklch(60% 0.16 80); + --chart-5: oklch(58% 0.14 330); +} + +[data-theme='ember-dark'] { + --font-family: var(--font-nunito); + --radius-base: 0.375rem; + --border-width: 1px; + + /* Deep charred black with warm undertone */ + --background: oklch(8% 0.025 25); + --foreground: oklch(92% 0.02 40); + + /* Slightly warmer card */ + --card: oklch(12% 0.03 25); + --card-foreground: oklch(92% 0.02 40); + + --popover: oklch(12% 0.03 25); + --popover-foreground: oklch(92% 0.02 40); + + /* Bright molten orange-red primary */ + --primary: oklch(68% 0.22 35); + --primary-foreground: oklch(8% 0.025 25); + + --secondary: oklch(18% 0.035 30); + --secondary-foreground: oklch(92% 0.02 40); + + --muted: oklch(16% 0.025 25); + --muted-foreground: oklch(60% 0.04 40); + + /* Deep ember accent */ + --accent: oklch(15% 0.03 30); + --accent-foreground: oklch(92% 0.02 40); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(96% 0.01 25); + + --border: oklch(22% 0.035 30); + --input: oklch(22% 0.035 30); + --ring: oklch(68% 0.22 35); + + --sidebar-background: oklch(6% 0.02 25); + --sidebar-foreground: oklch(65% 0.04 40); + --sidebar-primary: oklch(68% 0.22 35); + --sidebar-primary-foreground: oklch(8% 0.025 25); + --sidebar-accent: oklch(14% 0.03 30); + --sidebar-accent-foreground: oklch(92% 0.02 40); + --sidebar-border: oklch(20% 0.03 30); + --sidebar-ring: oklch(68% 0.22 35); + + --gradient-primary: linear-gradient(135deg, oklch(68% 0.22 35), oklch(60% 0.2 25)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(18% 0.05 25 / 0.6), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(68% 0.22 35 / 0.06) 50%, transparent 75%); + + /* Warm glow shadows */ + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.5); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.6), 0 2px 4px -2px oklch(0% 0 0 / 0.4); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.7), 0 4px 6px -4px oklch(0% 0 0 / 0.5); + --shadow-primary: 0 0 20px oklch(68% 0.22 35 / 0.4), 0 4px 14px -3px oklch(68% 0.22 35 / 0.5); + + --chart-1: oklch(68% 0.22 35); + --chart-2: oklch(72% 0.18 50); + --chart-3: oklch(60% 0.12 200); + --chart-4: oklch(65% 0.16 80); + --chart-5: oklch(62% 0.14 330); +} + +/* ============================================ + FROST - Arctic Ice & Crystalline Cold + Icy blues with silver whites and frozen edges + Font: Inter | Radius: Sharp angles | Borders: Crisp + + Design: Clean, sharp, ethereal. Like ice crystals + catching light in a frozen landscape. + ============================================ */ + +[data-theme='frost-light'] { + --font-family: var(--font-inter); + --radius-base: 0.25rem; + --border-width: 1px; + + /* Cool white with blue undertone */ + --background: oklch(98.5% 0.008 230); + --foreground: oklch(18% 0.02 230); + + /* Pure frost white cards */ + --card: oklch(100% 0 0); + --card-foreground: oklch(18% 0.02 230); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(18% 0.02 230); + + /* Brilliant ice blue primary */ + --primary: oklch(58% 0.14 230); + --primary-foreground: oklch(98% 0.01 230); + + /* Silver-blue secondary */ + --secondary: oklch(96% 0.01 230); + --secondary-foreground: oklch(25% 0.02 230); + + --muted: oklch(96% 0.008 230); + --muted-foreground: oklch(48% 0.015 230); + + /* Light frost accent */ + --accent: oklch(95% 0.012 230); + --accent-foreground: oklch(25% 0.02 230); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(92% 0.01 230); + --input: oklch(92% 0.01 230); + --ring: oklch(58% 0.14 230); + + --sidebar-background: oklch(97.5% 0.01 230); + --sidebar-foreground: oklch(35% 0.02 230); + --sidebar-primary: oklch(58% 0.14 230); + --sidebar-primary-foreground: oklch(98% 0.01 230); + --sidebar-accent: oklch(94% 0.012 230); + --sidebar-accent-foreground: oklch(25% 0.02 230); + --sidebar-border: oklch(92% 0.01 230); + --sidebar-ring: oklch(58% 0.14 230); + + --gradient-primary: linear-gradient(135deg, oklch(58% 0.14 230), oklch(62% 0.12 250)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(94% 0.02 230 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.6) 50%, transparent 75%); + + --shadow-sm: 0 1px 2px oklch(20% 0.02 230 / 0.05); + --shadow-md: 0 4px 6px -1px oklch(20% 0.02 230 / 0.07), 0 2px 4px -2px oklch(20% 0.02 230 / 0.04); + --shadow-lg: 0 10px 15px -3px oklch(20% 0.02 230 / 0.08), 0 4px 6px -4px oklch(20% 0.02 230 / 0.05); + --shadow-primary: 0 4px 14px -3px oklch(58% 0.14 230 / 0.35); + + --chart-1: oklch(58% 0.14 230); + --chart-2: oklch(65% 0.12 200); + --chart-3: oklch(55% 0.1 280); + --chart-4: oklch(60% 0.14 180); + --chart-5: oklch(58% 0.12 260); +} + +[data-theme='frost-dark'] { + --font-family: var(--font-inter); + --radius-base: 0.25rem; + --border-width: 1px; + + /* Deep frozen blue-black */ + --background: oklch(10% 0.025 230); + --foreground: oklch(95% 0.01 230); + + /* Darker ice card */ + --card: oklch(14% 0.025 230); + --card-foreground: oklch(95% 0.01 230); + + --popover: oklch(14% 0.025 230); + --popover-foreground: oklch(95% 0.01 230); + + /* Glowing ice blue primary */ + --primary: oklch(72% 0.14 220); + --primary-foreground: oklch(10% 0.025 220); + + --secondary: oklch(20% 0.025 230); + --secondary-foreground: oklch(95% 0.01 230); + + --muted: oklch(18% 0.02 230); + --muted-foreground: oklch(62% 0.015 230); + + /* Frozen accent */ + --accent: oklch(17% 0.025 230); + --accent-foreground: oklch(95% 0.01 230); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(95% 0.01 25); + + --border: oklch(24% 0.025 230); + --input: oklch(24% 0.025 230); + --ring: oklch(72% 0.14 220); + + --sidebar-background: oklch(8% 0.02 230); + --sidebar-foreground: oklch(68% 0.015 230); + --sidebar-primary: oklch(72% 0.14 220); + --sidebar-primary-foreground: oklch(10% 0.025 220); + --sidebar-accent: oklch(15% 0.025 230); + --sidebar-accent-foreground: oklch(95% 0.01 230); + --sidebar-border: oklch(22% 0.025 230); + --sidebar-ring: oklch(72% 0.14 220); + + --gradient-primary: linear-gradient(135deg, oklch(72% 0.14 220), oklch(65% 0.12 240)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(18% 0.03 230 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(72% 0.14 220 / 0.05) 50%, transparent 75%); + + /* Cold crystalline shadows with subtle glow */ + --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.4); + --shadow-md: 0 4px 6px -1px oklch(0% 0 0 / 0.5), 0 2px 4px -2px oklch(0% 0 0 / 0.35); + --shadow-lg: 0 10px 15px -3px oklch(0% 0 0 / 0.6), 0 4px 6px -4px oklch(0% 0 0 / 0.4); + --shadow-primary: 0 0 16px oklch(72% 0.14 220 / 0.35), 0 4px 14px -3px oklch(72% 0.14 220 / 0.4); + + --chart-1: oklch(72% 0.14 220); + --chart-2: oklch(68% 0.12 200); + --chart-3: oklch(60% 0.1 280); + --chart-4: oklch(65% 0.14 180); + --chart-5: oklch(62% 0.12 260); +} + +/* ============================================ + SAKURA - Japanese Cherry Blossom Elegance + Soft pinks with deep magentas and zen grays + Font: Source Sans 3 | Radius: Soft | Borders: Delicate + + Design: Elegant, serene, poetic. Like standing under + cherry blossom trees in a traditional Japanese garden. + ============================================ */ + +[data-theme='sakura-light'] { + --font-family: var(--font-source); + --radius-base: 0.625rem; + --border-width: 1px; + + /* Soft blush white */ + --background: oklch(98% 0.012 0); + --foreground: oklch(22% 0.025 340); + + /* Pearl white cards */ + --card: oklch(99.5% 0.008 350); + --card-foreground: oklch(22% 0.025 340); + + --popover: oklch(99.5% 0.008 350); + --popover-foreground: oklch(22% 0.025 340); + + /* Cherry blossom pink primary */ + --primary: oklch(62% 0.16 350); + --primary-foreground: oklch(98% 0.01 350); + + /* Soft petal secondary */ + --secondary: oklch(95% 0.018 350); + --secondary-foreground: oklch(28% 0.02 340); + + --muted: oklch(96% 0.012 355); + --muted-foreground: oklch(48% 0.02 340); + + /* Zen gray-green accent */ + --accent: oklch(94% 0.015 160); + --accent-foreground: oklch(28% 0.02 340); + + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 25); + + --border: oklch(91% 0.015 355); + --input: oklch(91% 0.015 355); + --ring: oklch(62% 0.16 350); + + --sidebar-background: oklch(97% 0.015 355); + --sidebar-foreground: oklch(35% 0.02 340); + --sidebar-primary: oklch(62% 0.16 350); + --sidebar-primary-foreground: oklch(98% 0.01 350); + --sidebar-accent: oklch(94% 0.018 355); + --sidebar-accent-foreground: oklch(28% 0.02 340); + --sidebar-border: oklch(91% 0.015 355); + --sidebar-ring: oklch(62% 0.16 350); + + --gradient-primary: linear-gradient(135deg, oklch(62% 0.16 350), oklch(58% 0.14 330)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(94% 0.025 350 / 0.4), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(100% 0 0 / 0.5) 50%, transparent 75%); + + --shadow-sm: 0 1px 3px oklch(30% 0.02 340 / 0.06); + --shadow-md: 0 4px 8px -1px oklch(30% 0.02 340 / 0.08), 0 2px 4px -2px oklch(30% 0.02 340 / 0.05); + --shadow-lg: 0 12px 20px -4px oklch(30% 0.02 340 / 0.1), 0 4px 8px -4px oklch(30% 0.02 340 / 0.06); + --shadow-primary: 0 4px 16px -3px oklch(62% 0.16 350 / 0.35); + + --chart-1: oklch(62% 0.16 350); + --chart-2: oklch(65% 0.12 160); + --chart-3: oklch(55% 0.1 280); + --chart-4: oklch(60% 0.14 80); + --chart-5: oklch(58% 0.15 320); +} + +[data-theme='sakura-dark'] { + --font-family: var(--font-source); + --radius-base: 0.625rem; + --border-width: 1px; + + /* Deep twilight with magenta undertone */ + --background: oklch(10% 0.025 340); + --foreground: oklch(94% 0.012 350); + + /* Dark plum cards */ + --card: oklch(14% 0.03 340); + --card-foreground: oklch(94% 0.012 350); + + --popover: oklch(14% 0.03 340); + --popover-foreground: oklch(94% 0.012 350); + + /* Luminous sakura pink primary */ + --primary: oklch(75% 0.15 350); + --primary-foreground: oklch(10% 0.025 340); + + --secondary: oklch(20% 0.03 340); + --secondary-foreground: oklch(94% 0.012 350); + + --muted: oklch(18% 0.025 340); + --muted-foreground: oklch(62% 0.02 350); + + /* Zen accent */ + --accent: oklch(16% 0.025 160); + --accent-foreground: oklch(94% 0.012 350); + + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(94% 0.012 25); + + --border: oklch(24% 0.03 340); + --input: oklch(24% 0.03 340); + --ring: oklch(75% 0.15 350); + + --sidebar-background: oklch(8% 0.02 340); + --sidebar-foreground: oklch(68% 0.02 350); + --sidebar-primary: oklch(75% 0.15 350); + --sidebar-primary-foreground: oklch(10% 0.025 340); + --sidebar-accent: oklch(14% 0.025 340); + --sidebar-accent-foreground: oklch(94% 0.012 350); + --sidebar-border: oklch(22% 0.025 340); + --sidebar-ring: oklch(75% 0.15 350); + + --gradient-primary: linear-gradient(135deg, oklch(75% 0.15 350), oklch(68% 0.13 330)); + --gradient-bg-radial: radial-gradient(ellipse at top, oklch(18% 0.04 340 / 0.5), transparent 70%); + --gradient-card-shine: linear-gradient(110deg, transparent 25%, oklch(75% 0.15 350 / 0.05) 50%, transparent 75%); + + /* Soft ethereal shadows */ + --shadow-sm: 0 1px 3px oklch(0% 0 0 / 0.4); + --shadow-md: 0 4px 8px -1px oklch(0% 0 0 / 0.5), 0 2px 4px -2px oklch(0% 0 0 / 0.35); + --shadow-lg: 0 12px 20px -4px oklch(0% 0 0 / 0.6), 0 4px 8px -4px oklch(0% 0 0 / 0.4); + --shadow-primary: 0 0 18px oklch(75% 0.15 350 / 0.35), 0 4px 16px -3px oklch(75% 0.15 350 / 0.4); + + --chart-1: oklch(75% 0.15 350); + --chart-2: oklch(68% 0.12 160); + --chart-3: oklch(60% 0.1 280); + --chart-4: oklch(65% 0.14 80); + --chart-5: oklch(62% 0.15 320); +} + /* Apply font family and radius globally */ [data-theme] body, :root body { From 471cbceca9775771e4460c1b3ea16f5f44f0ac67 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 03:28:15 +0530 Subject: [PATCH 21/26] feat: move version to sidebar with GitHub release link - Display version below OpNotes icon in sidebar - Make version clickable to open GitHub releases page - Remove version from header for cleaner UI --- src/renderer/src/components/layout/Header.tsx | 6 +----- src/renderer/src/components/layout/Sidebar.tsx | 13 +++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/layout/Header.tsx b/src/renderer/src/components/layout/Header.tsx index e744706..9920ec2 100644 --- a/src/renderer/src/components/layout/Header.tsx +++ b/src/renderer/src/components/layout/Header.tsx @@ -10,7 +10,7 @@ interface HeaderProps { } export function Header({ onSearchClick }: HeaderProps) { - const { appVersion, settings } = useSettings() + const { settings } = useSettings() const hospitalName = settings?.['hospital'] || '' const unitName = settings?.['unit'] || '' @@ -56,10 +56,6 @@ export function Header({ onSearchClick }: HeaderProps) { {/* Animated light/dark toggle */} - - {appVersion && ( - v{appVersion} - )}
    ) diff --git a/src/renderer/src/components/layout/Sidebar.tsx b/src/renderer/src/components/layout/Sidebar.tsx index 62a645d..c9ea09d 100644 --- a/src/renderer/src/components/layout/Sidebar.tsx +++ b/src/renderer/src/components/layout/Sidebar.tsx @@ -9,6 +9,7 @@ import { } from 'lucide-react' import opNotesLogo from '@renderer/assets/opnotes-logo.png' import wavezyncLogoDark from '../../../../../resources/wavezync-dark.png?asset' +import { useSettings } from '@renderer/contexts/SettingsContext' interface NavItemProps { to: string @@ -50,6 +51,8 @@ function NavItem({ to, icon, label, end, index = 0 }: NavItemProps) { } export function Sidebar() { + const { appVersion } = useSettings() + const mainNavItems = [ { to: '/', icon: , label: 'Home', end: true }, { to: '/patients', icon: , label: 'Patients' }, @@ -67,6 +70,16 @@ export function Sidebar() { className="h-8 w-8 rounded-lg shadow-theme-sm transition-transform duration-200 hover:scale-105" /> OpNotes + {appVersion && ( + + v{appVersion} + + )}
    {/* Main Navigation */} From 949d952893e76bbbb73e78e194b84e9d6b90b5c5 Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 03:38:08 +0530 Subject: [PATCH 22/26] feat: add configurable backup frequency with daily/weekly options Add user-configurable backup frequency (hourly, daily, weekly) with default of daily at 08:00 AM. Includes improved backup cleanup logging. --- src/main/backup.ts | 132 +++++++++++++++--- src/main/db/migrations.ts | 4 +- .../db/migrations/008_backup_frequency.ts | 20 +++ .../components/settings/BackupSettings.tsx | 76 +++++++++- src/shared/types/backup.d.ts | 4 + 5 files changed, 205 insertions(+), 31 deletions(-) create mode 100644 src/main/db/migrations/008_backup_frequency.ts diff --git a/src/main/backup.ts b/src/main/backup.ts index d70982d..87b789a 100644 --- a/src/main/backup.ts +++ b/src/main/backup.ts @@ -3,13 +3,13 @@ import { join } from 'path' import { copyFile, mkdir, readdir, stat, unlink } from 'fs/promises' import log from 'electron-log' import { DB_PATH, db } from './db' -import { BackupInfo, BackupReason, BackupResult } from '../shared/types/backup' +import { BackupFrequency, BackupInfo, BackupReason, BackupResult } from '../shared/types/backup' const DEFAULT_BACKUP_FOLDER = join(app.getPath('userData'), 'backups') const MAX_BACKUPS = 10 const HOURLY_MS = 60 * 60 * 1000 -let schedulerInterval: NodeJS.Timeout | null = null +let schedulerTimeout: NodeJS.Timeout | null = null function formatDate(date: Date): string { const pad = (n: number) => n.toString().padStart(2, '0') @@ -136,15 +136,21 @@ export async function cleanupOldBackups(maxToKeep: number = MAX_BACKUPS): Promis const backups = await listBackups() if (backups.length <= maxToKeep) { + log.info(`Backup cleanup: ${backups.length} backups, keeping all (max: ${maxToKeep})`) return } // Delete backups beyond the max limit (list is already sorted newest first) const toDelete = backups.slice(maxToKeep) + log.info(`Backup cleanup: Found ${backups.length} backups, deleting ${toDelete.length} oldest`) for (const backup of toDelete) { - await unlink(backup.path) - log.info(`Cleaned up old backup: ${backup.filename}`) + try { + await unlink(backup.path) + log.info(`Deleted old backup: ${backup.filename}`) + } catch (error) { + log.warn(`Failed to delete backup ${backup.filename}: ${error}`) + } } } catch (error) { log.error('Error cleaning up old backups:', error) @@ -171,6 +177,58 @@ async function getLastBackupTime(): Promise { return result?.value ? new Date(result.value) : null } +async function getBackupFrequency(): Promise { + const result = await db + .selectFrom('app_settings') + .select('value') + .where('key', '=', 'backup_frequency') + .executeTakeFirst() + + return (result?.value as BackupFrequency) || 'daily' +} + +async function getBackupTime(): Promise { + const result = await db + .selectFrom('app_settings') + .select('value') + .where('key', '=', 'backup_time') + .executeTakeFirst() + + return result?.value || '08:00' +} + +function getNextScheduledTime(frequency: BackupFrequency, time: string): Date { + const [hours, minutes] = time.split(':').map(Number) + const now = new Date() + const next = new Date() + + next.setHours(hours, minutes, 0, 0) + + if (frequency === 'hourly') { + // For hourly, just return next hour + next.setMinutes(0, 0, 0) + if (next <= now) { + next.setHours(next.getHours() + 1) + } + } else if (frequency === 'daily') { + // Run at specified time daily + if (next <= now) { + next.setDate(next.getDate() + 1) + } + } else if (frequency === 'weekly') { + // Run on Monday at specified time + const dayOfWeek = now.getDay() // 0 = Sunday, 1 = Monday, ... + const daysUntilMonday = dayOfWeek === 0 ? 1 : dayOfWeek === 1 ? 0 : 8 - dayOfWeek + next.setDate(now.getDate() + daysUntilMonday) + // If it's Monday but past the time, schedule for next Monday + if (next <= now) { + next.setDate(next.getDate() + 7) + } + } + + return next +} + export async function initBackupScheduler(): Promise { const enabled = await isBackupEnabled() if (!enabled) { @@ -178,32 +236,60 @@ export async function initBackupScheduler(): Promise { return } + const frequency = await getBackupFrequency() + const backupTime = await getBackupTime() const lastBackup = await getLastBackupTime() - const now = Date.now() + const now = new Date() let delay: number - if (lastBackup) { - const timeSinceLast = now - lastBackup.getTime() - if (timeSinceLast >= HOURLY_MS) { - // More than an hour since last backup, backup immediately - delay = 0 + if (frequency === 'hourly') { + // For hourly, use the old logic + if (lastBackup) { + const timeSinceLast = now.getTime() - lastBackup.getTime() + if (timeSinceLast >= HOURLY_MS) { + delay = 0 + } else { + delay = HOURLY_MS - timeSinceLast + } } else { - // Wait until the hour completes - delay = HOURLY_MS - timeSinceLast + delay = 0 } + log.info(`Backup scheduler starting (hourly). First backup in ${Math.round(delay / 1000)}s`) + scheduleNextBackup(delay, frequency, backupTime) } else { - // No previous backup, start immediately - delay = 0 - } + // For daily/weekly, calculate next scheduled time + const nextRun = getNextScheduledTime(frequency, backupTime) + delay = nextRun.getTime() - now.getTime() - log.info(`Backup scheduler starting. First backup in ${Math.round(delay / 1000)}s`) + // If no backup has ever been done, or last backup is older than the interval, run immediately + const intervalMs = frequency === 'daily' ? 24 * HOURLY_MS : 7 * 24 * HOURLY_MS + if (!lastBackup || now.getTime() - lastBackup.getTime() >= intervalMs) { + delay = 0 + log.info(`Backup scheduler starting (${frequency}). Running backup immediately (overdue)`) + } else { + log.info( + `Backup scheduler starting (${frequency} at ${backupTime}). Next backup: ${nextRun.toLocaleString()}` + ) + } + scheduleNextBackup(delay, frequency, backupTime) + } +} - // Schedule first backup - setTimeout(async () => { +function scheduleNextBackup(delay: number, frequency: BackupFrequency, backupTime: string): void { + schedulerTimeout = setTimeout(async () => { await runScheduledBackup() - // Then start hourly interval - schedulerInterval = setInterval(runScheduledBackup, HOURLY_MS) + + // Schedule next backup based on frequency + let nextDelay: number + if (frequency === 'hourly') { + nextDelay = HOURLY_MS + } else { + const nextRun = getNextScheduledTime(frequency, backupTime) + nextDelay = nextRun.getTime() - Date.now() + log.info(`Next ${frequency} backup scheduled for: ${nextRun.toLocaleString()}`) + } + scheduleNextBackup(nextDelay, frequency, backupTime) }, delay) } @@ -219,9 +305,9 @@ async function runScheduledBackup(): Promise { } export function stopBackupScheduler(): void { - if (schedulerInterval) { - clearInterval(schedulerInterval) - schedulerInterval = null + if (schedulerTimeout) { + clearTimeout(schedulerTimeout) + schedulerTimeout = null log.info('Backup scheduler stopped') } } diff --git a/src/main/db/migrations.ts b/src/main/db/migrations.ts index 1ced77b..5f95d4e 100644 --- a/src/main/db/migrations.ts +++ b/src/main/db/migrations.ts @@ -6,6 +6,7 @@ import * as m_004_surgery_templates from './migrations/004_surgery_templates' import * as m_005_backup_settings from './migrations/005_backup_settings' import * as m_006_onboarding_setting from './migrations/006_onboarding_setting' import * as m_007_activity_log from './migrations/007_activity_log' +import * as m_008_backup_frequency from './migrations/008_backup_frequency' export default { '000_init': m_000_init, @@ -15,5 +16,6 @@ export default { '004_surgery_templates': m_004_surgery_templates, '005_backup_settings': m_005_backup_settings, '006_onboarding_setting': m_006_onboarding_setting, - '007_activity_log': m_007_activity_log + '007_activity_log': m_007_activity_log, + '008_backup_frequency': m_008_backup_frequency } diff --git a/src/main/db/migrations/008_backup_frequency.ts b/src/main/db/migrations/008_backup_frequency.ts new file mode 100644 index 0000000..8e0f4e1 --- /dev/null +++ b/src/main/db/migrations/008_backup_frequency.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + // Insert backup frequency and time settings + await db + .insertInto('app_settings') + .values([ + { key: 'backup_frequency', value: 'daily', display_name: 'Backup Frequency' }, + { key: 'backup_time', value: '08:00', display_name: 'Backup Time' } + ]) + .execute() +} + +export async function down(db: Kysely): Promise { + await db + .deleteFrom('app_settings') + .where('key', 'in', ['backup_frequency', 'backup_time']) + .execute() +} diff --git a/src/renderer/src/components/settings/BackupSettings.tsx b/src/renderer/src/components/settings/BackupSettings.tsx index 525ffc3..992ce54 100644 --- a/src/renderer/src/components/settings/BackupSettings.tsx +++ b/src/renderer/src/components/settings/BackupSettings.tsx @@ -20,6 +20,13 @@ import { Input } from '@renderer/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card' import { Switch } from '@renderer/components/ui/switch' import { Label } from '@renderer/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@renderer/components/ui/select' import { AlertDialog, AlertDialogAction, @@ -33,7 +40,7 @@ import { import { queries } from '@renderer/lib/queries' import { useSettings } from '@renderer/contexts/SettingsContext' import { unwrapResult, cn } from '@renderer/lib/utils' -import { BackupInfo } from 'src/shared/types/backup' +import { BackupFrequency, BackupInfo } from 'src/shared/types/backup' import { Skeleton } from '@renderer/components/ui/skeleton' function formatBytes(bytes: number): string { @@ -59,6 +66,8 @@ export const BackupSettings = () => { const [backupEnabled, setBackupEnabled] = useState(true) const [backupFolder, setBackupFolder] = useState('') + const [backupFrequency, setBackupFrequency] = useState('daily') + const [backupTime, setBackupTime] = useState('08:00') const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [backupToDelete, setBackupToDelete] = useState(null) const [restoreDialogOpen, setRestoreDialogOpen] = useState(false) @@ -70,6 +79,8 @@ export const BackupSettings = () => { if (settings) { setBackupEnabled(settings['backup_enabled'] === 'true') setBackupFolder(settings['backup_folder'] || '') + setBackupFrequency((settings['backup_frequency'] as BackupFrequency) || 'daily') + setBackupTime(settings['backup_time'] || '08:00') } }, [settings]) @@ -165,7 +176,9 @@ export const BackupSettings = () => { const handleSaveSettings = async () => { await updateSettingsMutation.mutateAsync([ { key: 'backup_enabled', value: backupEnabled ? 'true' : 'false' }, - { key: 'backup_folder', value: backupFolder || null } + { key: 'backup_folder', value: backupFolder || null }, + { key: 'backup_frequency', value: backupFrequency }, + { key: 'backup_time', value: backupTime } ]) } @@ -229,11 +242,8 @@ export const BackupSettings = () => { {/* Enable Switch */}
    -
    + {/* Frequency and Time Settings */} + {backupEnabled && ( +
    + {/* Frequency Dropdown */} +
    +
    +
    + +
    + +
    + +
    + + {/* Time Picker (only for daily/weekly) */} +
    +
    +
    + +
    + +
    + setBackupTime(e.target.value)} + disabled={backupFrequency === 'hourly'} + className="h-11" + /> + {backupFrequency === 'hourly' && ( +

    + Hourly backups run every hour +

    + )} +
    +
    + )} + {/* Backup Folder */}
    diff --git a/src/shared/types/backup.d.ts b/src/shared/types/backup.d.ts index 65db52f..f7c2841 100644 --- a/src/shared/types/backup.d.ts +++ b/src/shared/types/backup.d.ts @@ -1,5 +1,7 @@ export type BackupReason = 'manual' | 'scheduled' | 'pre-update' +export type BackupFrequency = 'hourly' | 'daily' | 'weekly' + export interface BackupInfo { filename: string path: string @@ -16,5 +18,7 @@ export interface BackupResult { export interface BackupSettings { backup_enabled: boolean backup_folder: string | null + backup_frequency: BackupFrequency + backup_time: string // HH:mm format last_backup_time: string | null } From c2b2bc7ed5b9e570ef87c8821900e7d9adb6192b Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 03:41:30 +0530 Subject: [PATCH 23/26] feat: use light WaveZync logo for light themes Switch between wavezync-light.png and wavezync-dark.png based on current theme mode in Sidebar and PoweredBy components. For splash screen, use system color scheme preference. --- .../src/components/brand/PowredBy.tsx | 24 ++++++++++++------- .../src/components/layout/Sidebar.tsx | 6 ++++- src/renderer/src/routes/root.tsx | 6 ++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/components/brand/PowredBy.tsx b/src/renderer/src/components/brand/PowredBy.tsx index b20dd51..1e5fa10 100644 --- a/src/renderer/src/components/brand/PowredBy.tsx +++ b/src/renderer/src/components/brand/PowredBy.tsx @@ -1,11 +1,17 @@ import wavezyncLogoDark from '../../../../../resources/wavezync-dark.png?asset' -// import wavezyncLogoLight from '../../../../resources/wavezync-light.png?asset' +import wavezyncLogoLight from '../../../../../resources/wavezync-light.png?asset' +import { useTheme } from '@renderer/contexts/ThemeContext' -export const PoweredBy = () => ( -
    -
    Powered by
    - - Wavezync - -
    -) +export const PoweredBy = () => { + const { theme } = useTheme() + const wavezyncLogo = theme.mode === 'light' ? wavezyncLogoLight : wavezyncLogoDark + + return ( +
    +
    Powered by
    + + Wavezync + +
    + ) +} diff --git a/src/renderer/src/components/layout/Sidebar.tsx b/src/renderer/src/components/layout/Sidebar.tsx index c9ea09d..6d2a1e8 100644 --- a/src/renderer/src/components/layout/Sidebar.tsx +++ b/src/renderer/src/components/layout/Sidebar.tsx @@ -9,7 +9,9 @@ import { } from 'lucide-react' import opNotesLogo from '@renderer/assets/opnotes-logo.png' import wavezyncLogoDark from '../../../../../resources/wavezync-dark.png?asset' +import wavezyncLogoLight from '../../../../../resources/wavezync-light.png?asset' import { useSettings } from '@renderer/contexts/SettingsContext' +import { useTheme } from '@renderer/contexts/ThemeContext' interface NavItemProps { to: string @@ -52,6 +54,8 @@ function NavItem({ to, icon, label, end, index = 0 }: NavItemProps) { export function Sidebar() { const { appVersion } = useSettings() + const { theme } = useTheme() + const wavezyncLogo = theme.mode === 'light' ? wavezyncLogoLight : wavezyncLogoDark const mainNavItems = [ { to: '/', icon: , label: 'Home', end: true }, @@ -114,7 +118,7 @@ export function Sidebar() { className="group flex flex-col items-center gap-1 py-3 px-2 border-t border-sidebar-border/50 transition-all duration-300 hover:bg-sidebar-accent/30" > WaveZync diff --git a/src/renderer/src/routes/root.tsx b/src/renderer/src/routes/root.tsx index f727eff..d9a4e96 100644 --- a/src/renderer/src/routes/root.tsx +++ b/src/renderer/src/routes/root.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery } from '@tanstack/react-query' import { queries } from '@renderer/lib/queries' import opnotesLogo from '../assets/opnotes-logo.png?asset' import wavezyncLogoDark from '../../../../resources/wavezync-dark.png?asset' +import wavezyncLogoLight from '../../../../resources/wavezync-light.png?asset' import { AlertCircle } from 'lucide-react' import { useEffect } from 'react' @@ -59,6 +60,9 @@ const SplashScreen = ({ error }: { error?: string }) => { ...queries.app.version }) const hasError = !!error + // Use system preference for splash since ThemeProvider isn't available yet + const isSystemDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? true + const wavezyncLogo = isSystemDark ? wavezyncLogoDark : wavezyncLogoLight return (
    @@ -186,7 +190,7 @@ const SplashScreen = ({ error }: { error?: string }) => { rel="noreferrer" className="opacity-70 hover:opacity-100 transition-opacity" > - WaveZync + WaveZync
    From 596ee3e1aba53efb3a7a0ba770f85a3a0f44c2ed Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 03:47:12 +0530 Subject: [PATCH 24/26] =?UTF-8?q?feat:=20show=20Ctrl=20instead=20of=20?= =?UTF-8?q?=E2=8C=98=20for=20keyboard=20shortcuts=20on=20Windows/Linux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose process.platform from Electron via preload and use it to dynamically display the correct modifier key symbol based on OS. --- src/preload/index.d.ts | 1 + src/preload/index.ts | 1 + .../keyboard/KeyboardShortcutsHelp.tsx | 3 ++- src/renderer/src/components/layout/Header.tsx | 3 ++- .../onboarding/steps/QuickTipsStep.tsx | 11 ++++++----- src/renderer/src/hooks/useKeyboardShortcuts.ts | 7 ++++--- src/renderer/src/lib/platform.ts | 18 ++++++++++++++++++ 7 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 src/renderer/src/lib/platform.ts diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index e7ddae1..4db142f 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -3,6 +3,7 @@ import type { ApiType } from '../main/api' import { PrintDialogArgs, UpdateStatusPayload } from './interfaces' type ElectronApi = ElectronAPI & { + platform: NodeJS.Platform getAppVersion: () => Promise boot: () => Promise openPrintDialog: (options: PrintDialogArgs) => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index f235a8c..920eb40 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -75,6 +75,7 @@ const restartApp = async (): Promise => { const electronApi = { ...electronAPI, + platform: process.platform, getAppVersion, boot, openPrintDialog, diff --git a/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx b/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx index 1339fd2..a79cab1 100644 --- a/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx +++ b/src/renderer/src/components/keyboard/KeyboardShortcutsHelp.tsx @@ -6,6 +6,7 @@ import { DialogTitle } from '@renderer/components/ui/dialog' import { keyboardShortcuts } from '@renderer/hooks/useKeyboardShortcuts' +import { modKey } from '@renderer/lib/platform' interface KeyboardShortcutsHelpProps { open: boolean @@ -62,7 +63,7 @@ export function KeyboardShortcutsHelp({ open, onOpenChange }: KeyboardShortcutsH
    - Press + / to toggle this help + Press {modKey} + / to toggle this help
    diff --git a/src/renderer/src/components/layout/Header.tsx b/src/renderer/src/components/layout/Header.tsx index 9920ec2..84eb37b 100644 --- a/src/renderer/src/components/layout/Header.tsx +++ b/src/renderer/src/components/layout/Header.tsx @@ -4,6 +4,7 @@ import { useSettings } from '@renderer/contexts/SettingsContext' import { UpdateIndicator } from '@renderer/components/update/UpdateIndicator' import { ThemeDropdown } from '@renderer/components/theme/ThemeDropdown' import { ModeToggle } from '@renderer/components/theme/ModeToggle' +import { modKey } from '@renderer/lib/platform' interface HeaderProps { onSearchClick?: () => void @@ -43,7 +44,7 @@ export function Header({ onSearchClick }: HeaderProps) { Search patients, surgeries... - K + {modKey}K diff --git a/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx b/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx index cc8e05b..b770330 100644 --- a/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx +++ b/src/renderer/src/components/onboarding/steps/QuickTipsStep.tsx @@ -1,17 +1,18 @@ -import { Sparkles, Search, Printer, Users, CalendarCheck, Keyboard, Command } from 'lucide-react' +import { Sparkles, Search, Printer, Users, CalendarCheck, Keyboard } from 'lucide-react' +import { modKey } from '@renderer/lib/platform' const tips = [ { icon: Search, title: 'Powerful Search', description: 'Find patients and surgeries instantly by name, BHT, diagnosis, or procedure', - shortcut: '⌘ + K' + shortcut: `${modKey} + K` }, { icon: Printer, title: 'Print Documents', description: 'Generate professional surgical notes and reports with one click', - shortcut: '⌘ + P' + shortcut: `${modKey} + P` }, { icon: Users, @@ -83,8 +84,8 @@ export const QuickTipsStep = () => { >
    -
    - +
    + {modKey}
    +
    diff --git a/src/renderer/src/hooks/useKeyboardShortcuts.ts b/src/renderer/src/hooks/useKeyboardShortcuts.ts index 06ced23..70afd89 100644 --- a/src/renderer/src/hooks/useKeyboardShortcuts.ts +++ b/src/renderer/src/hooks/useKeyboardShortcuts.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef } from 'react' import { useNavigate, useLocation } from 'react-router-dom' +import { modKey } from '@renderer/lib/platform' interface KeyboardShortcutsOptions { onOpenCommandPalette?: () => void @@ -126,9 +127,9 @@ export const keyboardShortcuts = [ { category: 'General', shortcuts: [ - { keys: ['⌘', 'K'], description: 'Open command palette' }, - { keys: ['⌘', '/'], description: 'Show keyboard shortcuts' }, - { keys: ['⌘', 'N'], description: 'New item (context-aware)' }, + { keys: [modKey, 'K'], description: 'Open command palette' }, + { keys: [modKey, '/'], description: 'Show keyboard shortcuts' }, + { keys: [modKey, 'N'], description: 'New item (context-aware)' }, { keys: ['Esc'], description: 'Close dialog / Cancel' } ] }, diff --git a/src/renderer/src/lib/platform.ts b/src/renderer/src/lib/platform.ts new file mode 100644 index 0000000..174404e --- /dev/null +++ b/src/renderer/src/lib/platform.ts @@ -0,0 +1,18 @@ +/** + * Platform detection utilities for keyboard shortcuts display + * Uses Electron's process.platform exposed via preload + */ + +const platform = window.electronApi?.platform + +/** + * Whether the current platform is macOS + */ +export const isMac = platform === 'darwin' + +/** + * Returns the appropriate modifier key symbol for the current platform + * - Mac: ⌘ (Command) + * - Windows/Linux: Ctrl + */ +export const modKey = isMac ? '⌘' : 'Ctrl' From ecbc1ac5b252daee09814767f5cf41c9d9937c9f Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 04:03:29 +0530 Subject: [PATCH 25/26] feat: add paginated activity log page with filters Add a new Activity page accessible from the sidebar that displays all system activity with filtering and pagination support. Users can filter by entity type, action type, and search text. --- src/main/api.ts | 5 +- src/main/repository/activity.ts | 80 ++++ .../src/components/layout/Sidebar.tsx | 4 +- src/renderer/src/lib/queries.ts | 7 + src/renderer/src/main.tsx | 6 + .../src/routes/activity/activity-index.tsx | 389 ++++++++++++++++++ src/shared/types/api.d.ts | 10 + 7 files changed, 498 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/routes/activity/activity-index.tsx diff --git a/src/main/api.ts b/src/main/api.ts index 2ac80ac..9381770 100644 --- a/src/main/api.ts +++ b/src/main/api.ts @@ -46,7 +46,7 @@ import { } from './repository/surgery-template' import { createBackup, listBackups, deleteBackup, restoreBackup } from './backup' import { getDashboardStats } from './repository/dashboard' -import { getRecentActivities } from './repository/activity' +import { getRecentActivities, listActivityLog } from './repository/activity' // Custom APIs for renderer export const api = { @@ -96,7 +96,8 @@ export const api = { restoreBackup, getDashboardStats, - getRecentActivities + getRecentActivities, + listActivityLog } export type ApiType = typeof api diff --git a/src/main/repository/activity.ts b/src/main/repository/activity.ts index 1ce1050..f72d1ae 100644 --- a/src/main/repository/activity.ts +++ b/src/main/repository/activity.ts @@ -1,4 +1,5 @@ import { db } from '../db' +import { ActivityLogFilter } from '../../shared/types/api' export type EntityType = 'patient' | 'surgery' | 'followup' | 'doctor' export type ActionType = 'created' | 'updated' | 'deleted' @@ -101,3 +102,82 @@ export async function getActivitiesForEntity( : new Date(row.created_at as number).toISOString() })) } + +/** + * List activity log entries with filtering and pagination + */ +export async function listActivityLog( + filter: ActivityLogFilter = {} +): Promise<{ data: ActivityLogItem[]; total: number }> { + const { entityType, action, search, startDate, endDate, page = 0, pageSize = 10 } = filter + + let query = db.selectFrom('activity_log') + let countQuery = db.selectFrom('activity_log') + + // Apply entity type filter + if (entityType) { + query = query.where('entity_type', '=', entityType) + countQuery = countQuery.where('entity_type', '=', entityType) + } + + // Apply action filter + if (action) { + query = query.where('action', '=', action) + countQuery = countQuery.where('action', '=', action) + } + + // Apply search filter (searches title and description) + if (search) { + const searchPattern = `%${search}%` + query = query.where((eb) => + eb.or([eb('title', 'like', searchPattern), eb('description', 'like', searchPattern)]) + ) + countQuery = countQuery.where((eb) => + eb.or([eb('title', 'like', searchPattern), eb('description', 'like', searchPattern)]) + ) + } + + // Apply date range filter (convert timestamps to Date objects for comparison) + if (startDate) { + const startDateObj = new Date(startDate) + query = query.where('created_at', '>=', startDateObj) + countQuery = countQuery.where('created_at', '>=', startDateObj) + } + + if (endDate) { + const endDateObj = new Date(endDate) + query = query.where('created_at', '<=', endDateObj) + countQuery = countQuery.where('created_at', '<=', endDateObj) + } + + // Get total count + const countResult = await countQuery + .select((eb) => eb.fn.countAll().as('count')) + .executeTakeFirst() + const total = countResult?.count ?? 0 + + // Get paginated results + const results = await query + .selectAll() + .orderBy('created_at', 'desc') + .limit(pageSize) + .offset(page * pageSize) + .execute() + + const data = results.map((row) => ({ + id: row.id, + entityType: row.entity_type as EntityType, + entityId: row.entity_id, + action: row.action as ActionType, + title: row.title, + description: row.description, + patientId: row.patient_id, + surgeryId: row.surgery_id, + createdAt: + row.created_at instanceof Date + ? row.created_at.toISOString() + : new Date(row.created_at as number).toISOString() + })) + + return { data, total } +} diff --git a/src/renderer/src/components/layout/Sidebar.tsx b/src/renderer/src/components/layout/Sidebar.tsx index 6d2a1e8..045d165 100644 --- a/src/renderer/src/components/layout/Sidebar.tsx +++ b/src/renderer/src/components/layout/Sidebar.tsx @@ -5,6 +5,7 @@ import { Users, Stethoscope, UserCog, + ScrollText, Settings } from 'lucide-react' import opNotesLogo from '@renderer/assets/opnotes-logo.png' @@ -61,7 +62,8 @@ export function Sidebar() { { to: '/', icon: , label: 'Home', end: true }, { to: '/patients', icon: , label: 'Patients' }, { to: '/surgeries', icon: , label: 'Surgeries' }, - { to: '/doctors', icon: , label: 'Doctors' } + { to: '/doctors', icon: , label: 'Doctors' }, + { to: '/activity', icon: , label: 'Activity' } ] return ( diff --git a/src/renderer/src/lib/queries.ts b/src/renderer/src/lib/queries.ts index bc5880a..11a8d60 100644 --- a/src/renderer/src/lib/queries.ts +++ b/src/renderer/src/lib/queries.ts @@ -1,5 +1,6 @@ import { createQueryKeyStore } from '@lukemorales/query-key-factory' import { + ActivityLogFilter, DoctorFilter, PatientFilter, SurgeryFilter, @@ -94,5 +95,11 @@ export const queries = createQueryKeyStore({ queryKey: [limit], queryFn: () => unwrapResult(window.api.invoke('getRecentActivities', limit)) }) + }, + activityLog: { + list: (filter: ActivityLogFilter) => ({ + queryKey: [filter], + queryFn: () => unwrapResult(window.api.invoke('listActivityLog', filter)) + }) } }) diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index e2833b3..0e566f8 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -25,6 +25,7 @@ import { SurgierisIndex } from './routes/surgeries/list-surgeries' import { QuickAddSurgery } from './routes/surgeries/quick-add-surgery' import { SettingsIndex } from './routes/settings/settings-index' import { AddTemplatePage, EditTemplatePage } from './routes/settings/template-form-page' +import { ActivityIndex } from './routes/activity/activity-index' const queryClient = new QueryClient({ defaultOptions: { @@ -109,6 +110,11 @@ const router = createHashRouter([ element: }, + { + path: '/activity', + element: + }, + { path: '*', element: diff --git a/src/renderer/src/routes/activity/activity-index.tsx b/src/renderer/src/routes/activity/activity-index.tsx new file mode 100644 index 0000000..9b7f552 --- /dev/null +++ b/src/renderer/src/routes/activity/activity-index.tsx @@ -0,0 +1,389 @@ +import { Button } from '@renderer/components/ui/button' +import { Card, CardContent } from '@renderer/components/ui/card' +import { Input } from '@renderer/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@renderer/components/ui/select' +import { Skeleton } from '@renderer/components/ui/skeleton' +import { useBreadcrumbs } from '@renderer/contexts/BreadcrumbContext' +import { queries } from '@renderer/lib/queries' +import { cn } from '@renderer/lib/utils' +import { keepPreviousData, queryOptions, useQuery } from '@tanstack/react-query' +import { + ChevronLeft, + ChevronRight, + FileSearch, + RefreshCw, + ScrollText, + Search, + Stethoscope, + UserCog, + Users +} from 'lucide-react' +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { ActivityLogFilter } from 'src/shared/types/api' +import { ActivityLogItem } from 'src/main/repository/activity' + +type EntityType = NonNullable +type ActionType = NonNullable + +const ENTITY_OPTIONS: { value: EntityType | 'all'; label: string }[] = [ + { value: 'all', label: 'All Entities' }, + { value: 'patient', label: 'Patient' }, + { value: 'surgery', label: 'Surgery' }, + { value: 'followup', label: 'Follow-up' }, + { value: 'doctor', label: 'Doctor' } +] + +const ACTION_OPTIONS: { value: ActionType | 'all'; label: string }[] = [ + { value: 'all', label: 'All Actions' }, + { value: 'created', label: 'Created' }, + { value: 'updated', label: 'Updated' }, + { value: 'deleted', label: 'Deleted' } +] + +const activityListQuery = (filter: ActivityLogFilter) => + queryOptions({ + ...queries.activityLog.list(filter), + placeholderData: keepPreviousData + }) + +// Entity type icon component +function EntityIcon({ entityType }: { entityType: EntityType }) { + const iconConfig = { + patient: { icon: Users, bgColor: 'bg-blue-500/15', textColor: 'text-blue-600' }, + surgery: { icon: Stethoscope, bgColor: 'bg-emerald-500/15', textColor: 'text-emerald-600' }, + followup: { icon: RefreshCw, bgColor: 'bg-amber-500/15', textColor: 'text-amber-600' }, + doctor: { icon: UserCog, bgColor: 'bg-violet-500/15', textColor: 'text-violet-600' } + } + + const config = iconConfig[entityType] + const Icon = config.icon + + return ( +
    + +
    + ) +} + +// Action badge component +function ActionBadge({ action }: { action: ActionType }) { + const badgeConfig = { + created: { label: 'Created', className: 'bg-emerald-500/10 text-emerald-600' }, + updated: { label: 'Updated', className: 'bg-blue-500/10 text-blue-600' }, + deleted: { label: 'Deleted', className: 'bg-red-500/10 text-red-600' } + } + + const config = badgeConfig[action] + + return ( + + {config.label} + + ) +} + +// Entity type badge component +function EntityBadge({ entityType }: { entityType: EntityType }) { + const badgeConfig = { + patient: { label: 'Patient', className: 'bg-blue-500/10 text-blue-600' }, + surgery: { label: 'Surgery', className: 'bg-emerald-500/10 text-emerald-600' }, + followup: { label: 'Follow-up', className: 'bg-amber-500/10 text-amber-600' }, + doctor: { label: 'Doctor', className: 'bg-violet-500/10 text-violet-600' } + } + + const config = badgeConfig[entityType] + + return ( + + {config.label} + + ) +} + +// Activity row component +function ActivityRow({ + activity, + onNavigate +}: { + activity: ActivityLogItem + onNavigate: (path: string) => void +}) { + const handleClick = () => { + // Navigate based on entity type + if (activity.entityType === 'patient' && activity.patientId) { + onNavigate(`/patients/${activity.patientId}`) + } else if (activity.entityType === 'surgery' && activity.patientId && activity.surgeryId) { + onNavigate(`/patients/${activity.patientId}/surgeries/${activity.surgeryId}`) + } else if (activity.entityType === 'followup' && activity.patientId && activity.surgeryId) { + onNavigate(`/patients/${activity.patientId}/surgeries/${activity.surgeryId}`) + } else if (activity.entityType === 'doctor') { + onNavigate(`/doctors/${activity.entityId}/edit`) + } + } + + const formattedDate = new Date(activity.createdAt).toLocaleDateString() + const formattedTime = new Date(activity.createdAt).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + }) + + return ( +
    + + +
    +
    + {activity.title} + + +
    + {activity.description && ( +

    {activity.description}

    + )} +
    + +
    +
    When
    +
    {formattedDate}
    +
    {formattedTime}
    +
    +
    + ) +} + +// Loading skeleton for activity row +function ActivityRowSkeleton() { + return ( +
    + +
    + + +
    + +
    + ) +} + +// Empty state +function EmptyState({ search }: { search: string }) { + return ( +
    +
    + +
    +

    No activity found

    +

    + {search + ? `No results for "${search}". Try adjusting your search or filters.` + : 'No activity has been recorded yet. Activity will appear here as you create and modify records.'} +

    +
    + ) +} + +// Pagination controls +function Pagination({ + page, + pageSize, + total, + onPageChange +}: { + page: number + pageSize: number + total: number + onPageChange: (page: number) => void +}) { + const totalPages = Math.ceil(total / pageSize) + const start = page * pageSize + 1 + const end = Math.min((page + 1) * pageSize, total) + + if (total === 0) return null + + return ( +
    + + Showing {start} to{' '} + {end} of{' '} + {total} activities + +
    + + + Page {page + 1} of {totalPages} + + +
    +
    + ) +} + +export const ActivityIndex = () => { + const { setBreadcrumbs } = useBreadcrumbs() + const navigate = useNavigate() + + const [page, setPage] = useState(0) + const [pageSize] = useState(10) + const [search, setSearch] = useState('') + const [entityType, setEntityType] = useState('all') + const [action, setAction] = useState('all') + + const filter: ActivityLogFilter = { + search: search || undefined, + entityType: entityType === 'all' ? undefined : entityType, + action: action === 'all' ? undefined : action, + page, + pageSize + } + + const { data, isLoading } = useQuery(activityListQuery(filter)) + + useEffect(() => { + setBreadcrumbs([{ label: 'Activity', to: '/activity' }]) + }, [setBreadcrumbs]) + + const handleFilterChange = (setter: (value: T) => void) => { + return (value: T) => { + setter(value) + setPage(0) + } + } + + return ( +
    + {/* Header Section */} +
    +
    +
    + +
    +
    +

    Activity

    +

    + {data?.total ?? 0} activities recorded +

    +
    +
    +
    + + {/* Search & Filter Bar */} + + +
    + {/* Search Input */} +
    + + { + setSearch(e.target.value) + setPage(0) + }} + /> +
    + + {/* Filter Controls */} +
    + + +
    +
    +
    +
    + + {/* Activity List */} +
    + {isLoading ? ( + <> + {[...Array(5)].map((_, i) => ( + + ))} + + ) : data?.data.length === 0 ? ( + + ) : ( + data?.data.map((activity) => ( + + )) + )} +
    + + {/* Pagination */} + {!isLoading && data && data.total > 0 && ( + + )} +
    + ) +} diff --git a/src/shared/types/api.d.ts b/src/shared/types/api.d.ts index 695fdca..f7f2d26 100644 --- a/src/shared/types/api.d.ts +++ b/src/shared/types/api.d.ts @@ -38,3 +38,13 @@ export interface SurgeryTemplateFilter { pageSize?: number page?: number } + +export interface ActivityLogFilter { + entityType?: 'patient' | 'surgery' | 'followup' | 'doctor' + action?: 'created' | 'updated' | 'deleted' + search?: string + startDate?: number + endDate?: number + pageSize?: number + page?: number +} From 9a36163eef36d9d7b7a149c669730a657714db8f Mon Sep 17 00:00:00 2001 From: Kasun Vithanage Date: Mon, 19 Jan 2026 04:06:26 +0530 Subject: [PATCH 26/26] feat: increase default window size and add minimum constraints Default size is now 1100x750 (was 900x670) with minimum size of 900x600 to prevent the window from becoming too small. --- src/main/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 26e16a9..7826851 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,8 +13,10 @@ import { PrintDialogArgs } from '../preload/interfaces' function createWindow() { // Create the browser window. const mainWindow = new BrowserWindow({ - width: 900, - height: 670, + width: 1100, + height: 750, + minWidth: 900, + minHeight: 600, show: false, autoHideMenuBar: true, ...(process.platform === 'linux' ? { icon } : {}),