diff --git a/client/.prettierrc.json b/client/.prettierrc.json deleted file mode 100644 index 5ac85e2..0000000 --- a/client/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "printWidth": 100, - "singleQuote": true -} diff --git a/client/biome.json b/client/biome.json deleted file mode 100644 index a25324f..0000000 --- a/client/biome.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "formatter": { - "enabled": false - } -} diff --git a/client/e2e/annotations.spec.ts b/client/e2e/annotations.spec.ts deleted file mode 100644 index 28cce05..0000000 --- a/client/e2e/annotations.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Annotation E2E Tests - * - * Tests the annotation toolbar and annotation display - * functionality within transcript segments. - */ - -test.describe('Annotation Toolbar', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should show annotation buttons on segment hover', async ({ page }) => { - // Find transcript segments if they exist - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - const firstSegment = segments.first(); - await firstSegment.hover(); - - // Toolbar should appear on hover - await page.waitForTimeout(100); - const toolbar = firstSegment.locator('[class*="group-hover:block"]'); - // Toolbar may or may not be visible depending on meeting state - } - }); - - test('should have action item button', async ({ page }) => { - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - const actionItemButton = page.getByTitle('Action Item'); - // May or may not be visible depending on meeting state - } - }); - - test('should have decision button', async ({ page }) => { - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - const decisionButton = page.getByTitle('Decision'); - // May or may not be visible depending on meeting state - } - }); - - test('should have note button', async ({ page }) => { - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - const noteButton = page.getByTitle('Note'); - // May or may not be visible depending on meeting state - } - }); - - test('should have risk button', async ({ page }) => { - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - const riskButton = page.getByTitle('Risk'); - // May or may not be visible depending on meeting state - } - }); -}); - -test.describe('Annotation Display', () => { - test('should show annotation badges on segments', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Annotation badges would appear on segments with annotations - const annotationBadges = page.locator('[class*="inline-flex items-center gap-1"]'); - // Will be visible only if segments have annotations - }); - - test('should show annotation type icons', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Look for annotation type indicators - const annotationIcons = page.locator('svg'); - // SVG icons used throughout the interface - }); -}); - -test.describe('Annotation Note Input', () => { - test('should show text input for note annotations', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - - const noteButton = page.getByTitle('Note'); - if (await noteButton.isVisible()) { - await noteButton.click(); - - // Input field should appear - const noteInput = page.getByPlaceholder('Add note...'); - if (await noteInput.isVisible()) { - await expect(noteInput).toBeVisible(); - } - } - } - }); - - test('should close note input on Escape', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - - const noteButton = page.getByTitle('Note'); - if (await noteButton.isVisible()) { - await noteButton.click(); - - const noteInput = page.getByPlaceholder('Add note...'); - if (await noteInput.isVisible()) { - await page.keyboard.press('Escape'); - // Input should close - await page.waitForTimeout(100); - } - } - } - }); -}); - -test.describe('Annotation Removal', () => { - test('should have remove button on annotation badges', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Annotation badges have X buttons for removal - const removeButtons = page.locator('[class*="h-3 w-3"]'); - // Will be visible only if annotations exist - }); -}); - -test.describe('Annotation Accessibility', () => { - test('should have accessible toolbar buttons', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Toolbar buttons should have titles - const buttonsWithTitles = page.locator('button[title]'); - const count = await buttonsWithTitles.count(); - expect(count).toBeGreaterThanOrEqual(0); - }); - - test('should support keyboard interaction for note input', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const segments = page.locator('[class*="group rounded-lg"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - - const noteButton = page.getByTitle('Note'); - if (await noteButton.isVisible()) { - await noteButton.click(); - - const noteInput = page.getByPlaceholder('Add note...'); - if (await noteInput.isVisible()) { - // Should be focusable and typable - await noteInput.focus(); - await page.keyboard.type('Test note'); - await expect(noteInput).toHaveValue('Test note'); - } - } - } - }); -}); diff --git a/client/e2e/app.spec.ts b/client/e2e/app.spec.ts deleted file mode 100644 index 04db5ce..0000000 --- a/client/e2e/app.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Core application E2E tests - * - * These tests verify basic application structure and navigation. - * Note: Tauri-specific functionality (IPC, system tray) cannot be tested - * in web mode and would require Tauri's WebDriver integration. - */ - -test.describe('Application', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should display NoteFlow header', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'NoteFlow' })).toBeVisible(); - }); - - test('should show connection panel', async ({ page }) => { - // Should show either Connected or Disconnected status - const connectionStatus = page.getByText(/Connected|Disconnected/); - await expect(connectionStatus).toBeVisible(); - }); - - test('should have navigation buttons', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Recording', exact: true })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Library' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Settings' })).toBeVisible(); - }); -}); - -test.describe('Navigation', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should switch to Recording view', async ({ page }) => { - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - // Recording view should be active - await expect(page.getByRole('button', { name: 'Recording', exact: true })).toHaveClass(/active|selected|bg-/); - }); - - test('should switch to Settings view', async ({ page }) => { - await page.getByRole('button', { name: 'Settings' }).click(); - await expect(page.getByRole('button', { name: 'Settings' })).toHaveClass(/active|selected|bg-/); - }); - - test('should switch to Library view', async ({ page }) => { - await page.getByRole('button', { name: 'Library' }).click(); - await expect(page.getByRole('button', { name: 'Library' })).toHaveClass(/active|selected|bg-/); - }); -}); - -test.describe('Recording View', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should show VU meter when available', async ({ page }) => { - // VU meter displays dB level - const vuMeter = page.getByText(/dB/); - // May or may not be visible depending on connection state - if (await vuMeter.isVisible()) { - await expect(vuMeter).toBeVisible(); - } - }); -}); - -test.describe('Library View', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - }); - - test('should show meeting library interface', async ({ page }) => { - // Library view should show some indication of meeting list - // This could be an empty state or meeting cards - const libraryContent = page.locator('main'); - await expect(libraryContent).toBeVisible(); - }); -}); - -test.describe('Accessibility', () => { - test('should have proper heading structure', async ({ page }) => { - await page.goto('/'); - // Should have exactly one h1 - const h1Elements = page.locator('h1'); - await expect(h1Elements).toHaveCount(1); - }); - - test('should have accessible navigation buttons', async ({ page }) => { - await page.goto('/'); - const buttons = page.getByRole('button'); - // All buttons should have accessible names - const buttonCount = await buttons.count(); - expect(buttonCount).toBeGreaterThan(0); - }); - - test('should be keyboard navigable', async ({ page }) => { - await page.goto('/'); - // Tab through the interface - await page.keyboard.press('Tab'); - // Something should be focused - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); -}); diff --git a/client/e2e/connection.spec.ts b/client/e2e/connection.spec.ts deleted file mode 100644 index cd4dd7d..0000000 --- a/client/e2e/connection.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Connection state E2E tests - * - * These tests verify the connection panel behavior in web mode. - * Note: Actual gRPC connections require the Tauri runtime. - */ - -test.describe('Connection Panel', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should show initial disconnected state', async ({ page }) => { - // In web mode without Tauri, should show disconnected - const status = page.getByText('Disconnected'); - await expect(status).toBeVisible(); - }); - - test('should show reconnect button when disconnected', async ({ page }) => { - const reconnectButton = page.getByTitle('Connect to server'); - // May not be visible if already connected - if (await page.getByText('Disconnected').isVisible()) { - await expect(reconnectButton).toBeVisible(); - } - }); - - test('should attempt reconnection on button click', async ({ page }) => { - // Find the connect button if visible - const connectButton = page.getByTitle('Connect to server'); - - if (await connectButton.isVisible()) { - // Click should trigger connection attempt - await connectButton.click(); - // Button should show loading state (spinning animation) - // Note: In web mode, this will likely fail but UI should respond - await page.waitForTimeout(500); - } - }); -}); - -test.describe('Connection Status Indicator', () => { - test('should display appropriate icon for connection state', async ({ page }) => { - await page.goto('/'); - - // Should have either connected or disconnected indicator - const connectionArea = page.locator('.flex.items-center.gap-2').first(); - await expect(connectionArea).toBeVisible(); - - // Should contain status text - const hasStatus = await page.getByText(/Connected|Disconnected/).isVisible(); - expect(hasStatus).toBe(true); - }); - - test('should use correct color for disconnected state', async ({ page }) => { - await page.goto('/'); - - if (await page.getByText('Disconnected').isVisible()) { - // Disconnected should have red styling - const statusBadge = page.getByText('Disconnected').locator('..'); - await expect(statusBadge).toHaveClass(/red/); - } - }); -}); diff --git a/client/e2e/fixtures.ts b/client/e2e/fixtures.ts deleted file mode 100644 index 2380a48..0000000 --- a/client/e2e/fixtures.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { test as base, expect } from '@playwright/test'; - -/** - * Custom fixtures for NoteFlow E2E tests - * - * These fixtures provide common utilities and mock data - * for testing the application in web mode. - */ - -// Mock data for tests -export const mockMeeting = { - id: 'test-meeting-1', - title: 'Test Meeting', - state: 'completed', - created_at: '2024-01-01T10:00:00Z', - updated_at: '2024-01-01T11:00:00Z', - duration_secs: 3600, - segment_count: 10, - has_summary: true, -}; - -export const mockSegment = { - id: 'test-segment-1', - meeting_id: 'test-meeting-1', - speaker: 'Speaker 1', - text: 'This is a test transcript segment.', - start_time: 0.0, - end_time: 5.0, - confidence: 0.95, - is_partial: false, -}; - -export const mockServerInfo = { - version: '1.0.0', - capabilities: ['streaming', 'diarization', 'summarization'], - asr_model: 'whisper-large', -}; - -// Extended test with custom fixtures -export const test = base.extend<{ - connectedState: void; - disconnectedState: void; -}>({ - // Fixture to ensure disconnected state - disconnectedState: async ({ page }, use) => { - await page.goto('/'); - // In web mode, we're always disconnected since no Tauri runtime - await use(); - }, - - // Fixture for connected state (would need mock server) - connectedState: async ({ page }, use) => { - // Note: This would require a mock WebSocket/gRPC server - // For now, just navigate to page - await page.goto('/'); - await use(); - }, -}); - -export { expect }; - -// Helper functions for tests -export async function waitForAppReady(page: import('@playwright/test').Page) { - // Wait for the main app to be rendered - await page.waitForSelector('h1:has-text("NoteFlow")'); -} - -export async function navigateToView( - page: import('@playwright/test').Page, - view: 'Recording' | 'Review' | 'Library' -) { - await page.getByRole('button', { name: view }).click(); - // Wait for navigation to complete - await page.waitForTimeout(100); -} - -export async function getConnectionStatus(page: import('@playwright/test').Page) { - const connected = await page.getByText('Connected').isVisible(); - return connected ? 'connected' : 'disconnected'; -} diff --git a/client/e2e/keyboard-navigation.spec.ts b/client/e2e/keyboard-navigation.spec.ts deleted file mode 100644 index f5a9677..0000000 --- a/client/e2e/keyboard-navigation.spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Keyboard Navigation E2E Tests - * - * Comprehensive tests for keyboard accessibility across - * all views and components. - */ - -test.describe('Global Keyboard Navigation', () => { - test('should start with focus on interactive element', async ({ page }) => { - await page.goto('/'); - - // Tab should move focus to first interactive element - await page.keyboard.press('Tab'); - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); - - test('should navigate through header buttons', async ({ page }) => { - await page.goto('/'); - - // Tab through header navigation - for (let i = 0; i < 5; i++) { - await page.keyboard.press('Tab'); - } - - // Should be able to reach navigation buttons - const navButtons = page.getByRole('button'); - const count = await navButtons.count(); - expect(count).toBeGreaterThan(0); - }); - - test('should activate buttons with Enter key', async ({ page }) => { - await page.goto('/'); - - // Focus on Library button and activate - const libraryButton = page.getByRole('button', { name: 'Library' }); - await libraryButton.focus(); - await page.keyboard.press('Enter'); - - // Should switch to library view - await expect(libraryButton).toHaveClass(/bg-primary/); - }); - - test('should activate buttons with Space key', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByRole('button', { name: 'Settings' }); - await settingsButton.focus(); - await page.keyboard.press('Space'); - - await expect(settingsButton).toHaveClass(/bg-primary/); - }); -}); - -test.describe('Recording View Keyboard Navigation', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should navigate to recording controls', async ({ page }) => { - // Tab through to find recording controls - for (let i = 0; i < 10; i++) { - await page.keyboard.press('Tab'); - } - - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); - - test('should navigate to playback slider if present', async ({ page }) => { - const slider = page.getByRole('slider'); - - if (await slider.isVisible()) { - await slider.focus(); - await expect(slider).toBeFocused(); - - // Arrow keys should change value - await page.keyboard.press('ArrowRight'); - // Value may change depending on initial state - } - }); -}); - -test.describe('Library View Keyboard Navigation', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - }); - - test('should focus search input on Tab', async ({ page }) => { - const searchInput = page.getByPlaceholder('Search meetings...'); - - // Tab until search is focused - for (let i = 0; i < 5; i++) { - await page.keyboard.press('Tab'); - if (await searchInput.evaluate((el) => el === document.activeElement)) { - break; - } - } - - // Should be able to type in search - await searchInput.focus(); - await page.keyboard.type('test'); - await expect(searchInput).toHaveValue('test'); - }); - - test('should navigate to refresh button', async ({ page }) => { - const refreshButton = page.getByRole('button', { name: /Refresh/i }); - - if (await refreshButton.isVisible()) { - await refreshButton.focus(); - await expect(refreshButton).toBeFocused(); - } - }); - - test('should navigate through meeting cards', async ({ page }) => { - const meetingCards = page.locator('[class*="cursor-pointer"]'); - - if ((await meetingCards.count()) > 0) { - // Tab through meeting cards - for (let i = 0; i < 10; i++) { - await page.keyboard.press('Tab'); - } - - const focused = page.locator(':focus'); - const isVisible = await focused.isVisible(); - expect(isVisible).toBe(true); - } - }); -}); - -test.describe('Settings View Keyboard Navigation', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - }); - - test('should navigate through settings fields', async ({ page }) => { - // Tab through settings form - for (let i = 0; i < 5; i++) { - await page.keyboard.press('Tab'); - } - - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); - - test('should allow text input in fields', async ({ page }) => { - const textInputs = page.locator('input[type="text"]'); - - if ((await textInputs.count()) > 0) { - const firstInput = textInputs.first(); - await firstInput.focus(); - await page.keyboard.type('test value'); - - const value = await firstInput.inputValue(); - expect(value).toContain('test value'); - } - }); - - test('should navigate checkboxes', async ({ page }) => { - const checkboxes = page.locator('input[type="checkbox"]'); - - if ((await checkboxes.count()) > 0) { - const firstCheckbox = checkboxes.first(); - await firstCheckbox.focus(); - - // Space should toggle checkbox - const initialState = await firstCheckbox.isChecked(); - await page.keyboard.press('Space'); - const newState = await firstCheckbox.isChecked(); - - expect(newState).not.toBe(initialState); - } - }); -}); - -test.describe('Dialog Keyboard Navigation', () => { - test('should close settings dialog with Escape', async ({ page }) => { - await page.goto('/'); - - // Settings view functions as a page, not a dialog - await page.getByRole('button', { name: 'Settings' }).click(); - - // Escape should return to previous view or be handled - await page.keyboard.press('Escape'); - - // May or may not change view depending on implementation - }); - - test('should trap focus in modal dialogs', async ({ page }) => { - await page.goto('/'); - - // If there's a trigger dialog, it should trap focus - const dialog = page.getByRole('dialog'); - - if (await dialog.isVisible()) { - // Tab should cycle within dialog - for (let i = 0; i < 20; i++) { - await page.keyboard.press('Tab'); - const focused = page.locator(':focus'); - // Focus should remain inside dialog - } - } - }); -}); - -test.describe('Focus Visibility', () => { - test('should show visible focus indicators', async ({ page }) => { - await page.goto('/'); - - // Tab to first button - await page.keyboard.press('Tab'); - - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - - // Focus ring should be visible (CSS outline or ring) - // This is a visual check - Playwright can verify element is focused - }); - - test('should maintain focus after view switch', async ({ page }) => { - await page.goto('/'); - - // Switch views and verify focus management - await page.getByRole('button', { name: 'Library' }).click(); - await page.keyboard.press('Tab'); - - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); -}); - -test.describe('Arrow Key Navigation', () => { - test('should navigate slider with arrow keys', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const slider = page.getByRole('slider'); - - if (await slider.isVisible()) { - await slider.focus(); - const initialValue = await slider.inputValue(); - - await page.keyboard.press('ArrowRight'); - const newValue = await slider.inputValue(); - - // Value should change (increase) - expect(parseInt(newValue)).toBeGreaterThanOrEqual(parseInt(initialValue)); - } - }); -}); - -test.describe('Skip Links', () => { - test('should skip to main content if skip link exists', async ({ page }) => { - await page.goto('/'); - - // Many apps have skip links as first focusable element - await page.keyboard.press('Tab'); - - const focused = page.locator(':focus'); - const text = await focused.textContent(); - - // Skip link would have text like "Skip to content" or "Skip to main" - // Not all apps implement this - }); -}); - -test.describe('Form Navigation', () => { - test('should navigate form fields in order', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Settings' }).click(); - - const inputs = page.locator('input'); - const inputCount = await inputs.count(); - - if (inputCount > 1) { - // Tab through inputs in order - const firstInput = inputs.first(); - await firstInput.focus(); - await page.keyboard.press('Tab'); - - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - } - }); - - test('should submit form with Enter', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - const searchInput = page.getByPlaceholder('Search meetings...'); - await searchInput.focus(); - await searchInput.fill('test search'); - await page.keyboard.press('Enter'); - - // Search should be applied (enter may or may not submit) - await expect(searchInput).toHaveValue('test search'); - }); -}); diff --git a/client/e2e/library.spec.ts b/client/e2e/library.spec.ts deleted file mode 100644 index 1c2b86b..0000000 --- a/client/e2e/library.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Meeting Library E2E Tests - * - * Tests the meeting library view including search, selection, - * export, and delete functionality. - */ - -test.describe('Meeting Library', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - }); - - test('should display library view', async ({ page }) => { - // Library view should be visible - const mainContent = page.locator('main'); - await expect(mainContent).toBeVisible(); - }); - - test('should show search input', async ({ page }) => { - const searchInput = page.getByPlaceholder('Search meetings...'); - await expect(searchInput).toBeVisible(); - }); - - test('should show refresh button', async ({ page }) => { - const refreshButton = page.getByRole('button', { name: /refresh/i }); - await expect(refreshButton).toBeVisible(); - }); - - test('should filter meetings on search input', async ({ page }) => { - const searchInput = page.getByPlaceholder('Search meetings...'); - await searchInput.fill('test'); - - // Should filter meetings (or show "No matching meetings found" if none match) - await page.waitForTimeout(300); - const content = page.locator('main'); - await expect(content).toBeVisible(); - }); - - test('should clear search on empty input', async ({ page }) => { - const searchInput = page.getByPlaceholder('Search meetings...'); - await searchInput.fill('test'); - await searchInput.fill(''); - - // Should show all meetings or empty state - await page.waitForTimeout(300); - const content = page.locator('main'); - await expect(content).toBeVisible(); - }); -}); - -test.describe('Library Empty State', () => { - test('should show empty state message when no meetings', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - // May show "No meetings yet" or meeting list - const emptyMessage = page.getByText(/No meetings yet|No matching meetings/); - const meetingCards = page.locator('[class*="cursor-pointer"]'); - - const hasEmpty = await emptyMessage.isVisible().catch(() => false); - const hasMeetings = (await meetingCards.count()) > 0; - - expect(hasEmpty || hasMeetings).toBe(true); - }); -}); - -test.describe('Library Meeting Actions', () => { - test('should show export button on meeting cards', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - // Find meeting cards if they exist - const meetingCards = page.locator('[class*="cursor-pointer"]').first(); - - if (await meetingCards.isVisible()) { - // Export button should be visible on hover - await meetingCards.hover(); - const exportButton = page.getByTitle('Export as Markdown'); - // May or may not be visible depending on CSS hover state - await page.waitForTimeout(100); - } - }); - - test('should show delete button on meeting cards', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - const meetingCards = page.locator('[class*="cursor-pointer"]').first(); - - if (await meetingCards.isVisible()) { - await meetingCards.hover(); - const deleteButton = page.getByTitle('Delete meeting'); - await page.waitForTimeout(100); - } - }); -}); - -test.describe('Library Keyboard Navigation', () => { - test('should focus search input first on Tab', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - // Focus the search input directly - const searchInput = page.getByPlaceholder('Search meetings...'); - await searchInput.focus(); - await expect(searchInput).toBeFocused(); - }); - - test('should allow typing in search when focused', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - const searchInput = page.getByPlaceholder('Search meetings...'); - await searchInput.focus(); - await page.keyboard.type('meeting'); - - await expect(searchInput).toHaveValue('meeting'); - }); -}); - -test.describe('Library Accessibility', () => { - test('should have accessible search input', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - const searchInput = page.getByPlaceholder('Search meetings...'); - await expect(searchInput).toHaveAttribute('type', 'text'); - }); - - test('should have accessible buttons', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Library' }).click(); - - const refreshButton = page.getByRole('button', { name: /refresh/i }); - await expect(refreshButton).toBeVisible(); - }); -}); diff --git a/client/e2e/playback.spec.ts b/client/e2e/playback.spec.ts deleted file mode 100644 index b7838e6..0000000 --- a/client/e2e/playback.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Playback E2E Tests - * - * Tests the playback controls UX including play/pause/stop - * and seek functionality. - */ - -test.describe('Playback Controls', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - // Recording view contains playback controls - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should show playback controls in Review view', async ({ page }) => { - // Playback controls area should exist - const playbackArea = page.locator('input[type="range"]'); - // May or may not be visible depending on whether a meeting is loaded - if (await playbackArea.isVisible()) { - await expect(playbackArea).toBeVisible(); - } - }); - - test('should display time in correct format', async ({ page }) => { - // Look for time display (00:00 format) - const timeDisplay = page.getByText(/\d{1,2}:\d{2}/); - // At least one time display should be present if playback is visible - const count = await timeDisplay.count(); - expect(count).toBeGreaterThanOrEqual(0); - }); -}); - -test.describe('Playback Keyboard Shortcuts', () => { - test('should be keyboard navigable', async ({ page }) => { - await page.goto('/'); - - // Tab through interface - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - - // Something should be focused - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); - - test('should support space bar for play/pause toggle', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Focus on a playback button if present - const playButton = page.locator('button').filter({ has: page.locator('svg') }).first(); - if (await playButton.isVisible()) { - await playButton.focus(); - // Space should toggle (in a real app with Tauri) - await page.keyboard.press('Space'); - } - }); -}); - -test.describe('Seek Bar Interaction', () => { - test('should have accessible slider', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const slider = page.getByRole('slider'); - if (await slider.isVisible()) { - await expect(slider).toBeVisible(); - // Should have min/max attributes - await expect(slider).toHaveAttribute('min', '0'); - } - }); - - test('should update position display on drag', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const slider = page.getByRole('slider'); - if (await slider.isVisible()) { - // Get initial value - const initialValue = await slider.inputValue(); - - // Simulate drag - await slider.fill('30'); - - // Value should change - const newValue = await slider.inputValue(); - expect(newValue).toBe('30'); - } - }); -}); diff --git a/client/e2e/recording.spec.ts b/client/e2e/recording.spec.ts deleted file mode 100644 index 9c9ac77..0000000 --- a/client/e2e/recording.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Recording E2E Tests - * - * Tests the recording flow including start/stop and - * connection state handling. - */ - -test.describe('Recording View', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should show recording controls', async ({ page }) => { - // Should have either Start Recording or Stop button - const startButton = page.getByRole('button', { name: /start recording/i }); - const stopButton = page.getByRole('button', { name: /stop/i }); - - const hasStart = await startButton.isVisible(); - const hasStop = await stopButton.isVisible(); - - // One of them should be visible - expect(hasStart || hasStop).toBe(true); - }); - - test('should show VU meter', async ({ page }) => { - // VU meter should display dB level - const vuMeter = page.getByText(/dB/); - await expect(vuMeter).toBeVisible(); - }); - - test('should disable recording when disconnected', async ({ page }) => { - // If disconnected, start button should be disabled - const disconnected = await page.getByText('Disconnected').isVisible(); - - if (disconnected) { - const startButton = page.getByRole('button', { name: /start recording/i }); - if (await startButton.isVisible()) { - await expect(startButton).toBeDisabled(); - } - } - }); -}); - -test.describe('Recording Timer', () => { - test('should show timer when recording', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // If recording is active, timer should show - const stopButton = page.getByRole('button', { name: /stop/i }); - if (await stopButton.isVisible()) { - // Look for time display (could be 00:00 or similar format) - const timeDisplay = page.getByText(/\d{1,2}:\d{2}(:\d{2})?/); - await expect(timeDisplay.first()).toBeVisible(); - } - }); -}); - -test.describe('Recording Start Flow', () => { - test('should show loading state when starting', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const startButton = page.getByRole('button', { name: /start recording/i }); - - if (await startButton.isVisible() && !(await startButton.isDisabled())) { - // Click should trigger loading state - await startButton.click(); - - // Button should become disabled during loading - // In web mode without Tauri, this will likely fail quickly - // but we test the UI response - await page.waitForTimeout(100); - } - }); -}); - -test.describe('Recording Stop Flow', () => { - test('should show stop button when recording', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // If we're recording, stop button should be visible - const stopButton = page.getByRole('button', { name: /stop/i }); - if (await stopButton.isVisible()) { - await expect(stopButton).toBeEnabled(); - } - }); -}); - -test.describe('VU Meter Visual Feedback', () => { - test('should display level indicator', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // VU meter should have segments - const vuContainer = page.locator('.h-4'); // Segment height class - const count = await vuContainer.count(); - expect(count).toBeGreaterThan(0); - }); - - test('should show dB value', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const dbDisplay = page.getByText(/-?\d+ dB/); - await expect(dbDisplay).toBeVisible(); - }); -}); diff --git a/client/e2e/settings.spec.ts b/client/e2e/settings.spec.ts deleted file mode 100644 index 038aabd..0000000 --- a/client/e2e/settings.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Settings E2E Tests - * - * Tests the settings panel UX including preferences, - * audio device selection, and connection settings. - */ - -test.describe('Settings Panel', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should open settings dialog', async ({ page }) => { - // Look for settings button (gear icon) - const settingsButton = page.getByTitle(/settings/i); - - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - // Settings dialog should open - const dialog = page.getByRole('dialog'); - await expect(dialog).toBeVisible(); - } - }); - - test('should close settings with Escape key', async ({ page }) => { - const settingsButton = page.getByTitle(/settings/i); - - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - const dialog = page.getByRole('dialog'); - await expect(dialog).toBeVisible(); - - // Press Escape to close - await page.keyboard.press('Escape'); - - await expect(dialog).not.toBeVisible(); - } - }); - - test('should close settings with close button', async ({ page }) => { - const settingsButton = page.getByTitle(/settings/i); - - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - const dialog = page.getByRole('dialog'); - await expect(dialog).toBeVisible(); - - // Find and click close button - const closeButton = dialog.locator('button').filter({ hasText: /close|cancel|×/i }).first(); - if (await closeButton.isVisible()) { - await closeButton.click(); - await expect(dialog).not.toBeVisible(); - } - } - }); -}); - -test.describe('Connection Settings', () => { - test('should show server address field', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - // Look for server URL input - const serverInput = page.getByLabel(/server/i); - if (await serverInput.isVisible()) { - await expect(serverInput).toBeVisible(); - } - } - }); - - test('should validate server address format', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - const serverInput = page.getByLabel(/server/i); - if (await serverInput.isVisible()) { - // Enter invalid address - await serverInput.fill('not-a-valid-url'); - - // Look for validation error - const error = page.getByText(/invalid|error/i); - // May or may not show error depending on validation timing - } - } - }); -}); - -test.describe('Audio Device Settings', () => { - test('should show device selection dropdown', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - // Look for audio device section - const audioSection = page.getByText(/audio|microphone|device/i); - if (await audioSection.first().isVisible()) { - await expect(audioSection.first()).toBeVisible(); - } - } - }); -}); - -test.describe('Data Directory Settings', () => { - test('should show data directory field', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - // Look for data directory input - const dataInput = page.getByLabel(/data|directory|folder/i); - if (await dataInput.first().isVisible()) { - await expect(dataInput.first()).toBeVisible(); - } - } - }); -}); - -test.describe('Settings Persistence', () => { - test('should preserve settings after close and reopen', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - // Open settings - await settingsButton.click(); - - const dialog = page.getByRole('dialog'); - await expect(dialog).toBeVisible(); - - // Find a text input - const input = dialog.locator('input[type="text"]').first(); - if (await input.isVisible()) { - const originalValue = await input.inputValue(); - - // Close and reopen - await page.keyboard.press('Escape'); - await settingsButton.click(); - - // Value should be preserved - const newValue = await input.inputValue(); - expect(newValue).toBe(originalValue); - } - } - }); -}); - -test.describe('Settings Accessibility', () => { - test('should have proper labels for inputs', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - const dialog = page.getByRole('dialog'); - if (await dialog.isVisible()) { - // All inputs should have labels - const inputs = dialog.locator('input'); - const count = await inputs.count(); - - for (let i = 0; i < count; i++) { - const input = inputs.nth(i); - // Should have either aria-label or associated label - const hasLabel = - (await input.getAttribute('aria-label')) || - (await input.getAttribute('id')); - // Not all inputs require labels, so we just check there's some accessibility - } - } - } - }); - - test('should be navigable with Tab key', async ({ page }) => { - await page.goto('/'); - - const settingsButton = page.getByTitle(/settings/i); - if (await settingsButton.isVisible()) { - await settingsButton.click(); - - const dialog = page.getByRole('dialog'); - if (await dialog.isVisible()) { - // Tab through elements - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - - // Something should be focused inside dialog - const focused = dialog.locator(':focus'); - const count = await focused.count(); - expect(count).toBe(1); - } - } - }); -}); diff --git a/client/e2e/summary.spec.ts b/client/e2e/summary.spec.ts deleted file mode 100644 index f271d3e..0000000 --- a/client/e2e/summary.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Summary Panel E2E Tests - * - * Tests the summary generation, display, and - * citation interaction features. - */ - -test.describe('Summary Panel', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should display summary heading', async ({ page }) => { - const summaryHeading = page.getByRole('heading', { name: 'Summary' }); - await expect(summaryHeading).toBeVisible(); - }); - - test('should show generate button', async ({ page }) => { - const generateButton = page.getByRole('button', { name: /Generate|Regenerate/i }); - // Button may or may not be visible depending on meeting state - const mainContent = page.locator('main'); - await expect(mainContent).toBeVisible(); - }); - - test('should show empty state when no summary', async ({ page }) => { - // Either show prompt to generate or record first - const emptyMessage = page.getByText(/Record a meeting|Click Generate|No transcript/); - const summaryContent = page.getByText(/Executive Summary/); - - const hasEmpty = await emptyMessage.first().isVisible().catch(() => false); - const hasSummary = await summaryContent.isVisible().catch(() => false); - - expect(hasEmpty || hasSummary).toBe(true); - }); -}); - -test.describe('Summary Generation', () => { - test('should disable generate button when no segments', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const generateButton = page.getByRole('button', { name: /Generate/i }); - - if (await generateButton.isVisible()) { - // Button should be disabled when no segments - const isDisabled = await generateButton.isDisabled(); - // May or may not be disabled depending on state - } - }); - - test('should show loading state during generation', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Loading indicator would appear during generation - const loadingIndicator = page.locator('[class*="animate-spin"]'); - // May or may not be visible - }); -}); - -test.describe('Summary Display', () => { - test('should display executive summary section', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Executive summary heading should exist in structure - const execSummaryLabel = page.getByText('Executive Summary'); - // Will be visible only if summary exists - }); - - test('should display key points section when available', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const keyPointsLabel = page.getByText('Key Points'); - // Will be visible only if summary has key points - }); - - test('should display action items section when available', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const actionItemsLabel = page.getByText('Action Items'); - // Will be visible only if summary has action items - }); -}); - -test.describe('Summary Citations', () => { - test('should show citation links on key points', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Citation links have format [#N] - const citationLinks = page.getByText(/\[#\d+\]/); - // Will be visible only if summary has citations - }); - - test('should make citation links clickable', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const citationLinks = page.locator('button').filter({ hasText: /\[#\d+\]/ }); - - if ((await citationLinks.count()) > 0) { - // Citation links should be interactive - await expect(citationLinks.first()).toBeVisible(); - } - }); -}); - -test.describe('Summary Action Items', () => { - test('should show assignee badges on action items', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Action items may have assignee badges - const assigneeBadges = page.locator('[class*="bg-primary/20"]'); - // Will be visible only if summary has action items with assignees - }); - - test('should show priority badges on action items', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Priority badges: High (red), Medium (yellow), Low (blue) - const priorityBadges = page.getByText(/High|Medium|Low/); - // Will be visible only if summary has prioritized action items - }); -}); - -test.describe('Summary Error Handling', () => { - test('should display error message on generation failure', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Error message would appear in red - const errorMessage = page.locator('[class*="bg-red"]'); - // May or may not be visible depending on state - }); -}); - -test.describe('Summary Scrolling', () => { - test('should have scrollable summary content', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const scrollableContent = page.locator('[class*="overflow-auto"]'); - await expect(scrollableContent.first()).toBeVisible(); - }); -}); - -test.describe('Summary Accessibility', () => { - test('should have proper heading hierarchy', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Summary heading should be h2 - const h2Elements = page.locator('h2'); - const count = await h2Elements.count(); - expect(count).toBeGreaterThanOrEqual(0); - }); - - test('should be keyboard navigable', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Summary heading should be visible - const summaryHeading = page.getByRole('heading', { name: 'Summary' }); - await expect(summaryHeading).toBeVisible(); - - // Main content area should be navigable - const mainContent = page.locator('main'); - await expect(mainContent).toBeVisible(); - }); -}); diff --git a/client/e2e/transcript.spec.ts b/client/e2e/transcript.spec.ts deleted file mode 100644 index 5593429..0000000 --- a/client/e2e/transcript.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * Transcript View E2E Tests - * - * Tests the transcript display, segment interaction, - * and annotation features. - */ - -test.describe('Transcript View', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - // Transcript is part of Recording view - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - }); - - test('should display transcript area', async ({ page }) => { - // Main content area should be visible - const mainContent = page.locator('main'); - await expect(mainContent).toBeVisible(); - }); - - test('should show empty state when no transcript', async ({ page }) => { - // Either show "Listening..." or "No transcript yet" - const emptyMessage = page.getByText(/Listening|No transcript yet/); - - // May or may not be visible depending on state - const hasEmptyState = await emptyMessage.isVisible().catch(() => false); - const hasSegments = (await page.locator('[class*="rounded-lg p-3"]').count()) > 0; - - expect(hasEmptyState || hasSegments).toBe(true); - }); - - test('should show partial text indicator when recording', async ({ page }) => { - // Partial text appears when ASR is processing - // This would show a dashed border element - const partialIndicator = page.locator('[class*="border-dashed"]'); - // May or may not be visible depending on recording state - await page.waitForTimeout(100); - }); -}); - -test.describe('Transcript Segment Display', () => { - test('should display speaker labels on segments', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // If segments exist, they should have speaker labels - const segments = page.locator('[class*="rounded-lg p-3"]'); - - if ((await segments.count()) > 0) { - // Speaker badge should have color styling - const speakerBadge = segments.first().locator('[class*="rounded text-xs"]'); - await expect(speakerBadge).toBeVisible(); - } - }); - - test('should display timestamps on segments', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const segments = page.locator('[class*="rounded-lg p-3"]'); - - if ((await segments.count()) > 0) { - // Timestamps should be visible (format: MM:SS - MM:SS) - const timestamp = segments.first().getByText(/\d{1,2}:\d{2}/); - await expect(timestamp.first()).toBeVisible(); - } - }); -}); - -test.describe('Transcript Click to Seek', () => { - test('should have clickable segments', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - const segments = page.locator('[class*="cursor-pointer"]'); - - if ((await segments.count()) > 0) { - // Segments should be clickable (have cursor-pointer) - const firstSegment = segments.first(); - await expect(firstSegment).toHaveClass(/cursor-pointer/); - } - }); -}); - -test.describe('Transcript Annotations', () => { - test('should show annotation toolbar on segment hover', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // If there's a current meeting and segments - const segments = page.locator('[class*="rounded-lg p-3"]'); - - if ((await segments.count()) > 0) { - await segments.first().hover(); - // Annotation toolbar should appear on hover - await page.waitForTimeout(100); - const toolbar = page.locator('[class*="group-hover:block"]'); - // May be visible if meeting is loaded - } - }); -}); - -test.describe('Transcript Auto-scroll', () => { - test('should have scrollable transcript container', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Transcript container should be scrollable - const transcriptContainer = page.locator('[class*="overflow-auto"]'); - await expect(transcriptContainer.first()).toBeVisible(); - }); -}); - -test.describe('Transcript Accessibility', () => { - test('should have proper text contrast', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Text should be visible and readable - const mainContent = page.locator('main'); - await expect(mainContent).toBeVisible(); - }); - - test('should be keyboard navigable', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // Tab through transcript area - await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - - const focused = page.locator(':focus'); - await expect(focused).toBeVisible(); - }); -}); - -test.describe('Transcript and Playback Integration', () => { - test('should highlight segment during playback', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Recording', exact: true }).click(); - - // If segments exist and playback is active - const highlightedSegment = page.locator('[class*="bg-primary/20"]'); - - // May or may not be visible depending on playback state - await page.waitForTimeout(100); - }); -}); diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 8af010b..0000000 --- a/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - NoteFlow - - -
- - - diff --git a/client/package-lock.json b/client/package-lock.json deleted file mode 100644 index fe67443..0000000 --- a/client/package-lock.json +++ /dev/null @@ -1,7223 +0,0 @@ -{ - "name": "noteflow-client", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "noteflow-client", - "version": "0.1.0", - "dependencies": { - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-slider": "^1.1.2", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-dialog": "^2.0.0", - "@tauri-apps/plugin-fs": "^2.0.0", - "@tauri-apps/plugin-notification": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "date-fns": "^3.3.0", - "immer": "^10.0.0", - "lucide-react": "^0.400.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-virtuoso": "^4.7.0", - "tailwind-merge": "^2.2.0", - "zustand": "^4.5.0" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.10", - "@playwright/test": "^1.41.0", - "@tauri-apps/cli": "^2.0.0", - "@testing-library/jest-dom": "^6.4.0", - "@testing-library/react": "^14.2.0", - "@testing-library/user-event": "^14.5.0", - "@types/node": "^25.0.3", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.2.0", - "@vitest/coverage-v8": "^1.2.0", - "@vitest/ui": "^1.2.0", - "autoprefixer": "^10.4.0", - "jsdom": "^24.0.0", - "postcss": "^8.4.0", - "prettier": "^3.7.4", - "tailwindcss": "^3.4.0", - "tailwindcss-animate": "^1.0.7", - "typescript": "^5.3.0", - "vite": "^5.1.0", - "vitest": "^1.2.0" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@biomejs/biome": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.10.tgz", - "integrity": "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.10", - "@biomejs/cli-darwin-x64": "2.3.10", - "@biomejs/cli-linux-arm64": "2.3.10", - "@biomejs/cli-linux-arm64-musl": "2.3.10", - "@biomejs/cli-linux-x64": "2.3.10", - "@biomejs/cli-linux-x64-musl": "2.3.10", - "@biomejs/cli-win32-arm64": "2.3.10", - "@biomejs/cli-win32-x64": "2.3.10" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz", - "integrity": "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz", - "integrity": "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz", - "integrity": "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz", - "integrity": "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", - "integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz", - "integrity": "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz", - "integrity": "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", - "integrity": "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@playwright/test": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", - "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "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 - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tauri-apps/api": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz", - "integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz", - "integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.9.6", - "@tauri-apps/cli-darwin-x64": "2.9.6", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", - "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", - "@tauri-apps/cli-linux-arm64-musl": "2.9.6", - "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", - "@tauri-apps/cli-linux-x64-gnu": "2.9.6", - "@tauri-apps/cli-linux-x64-musl": "2.9.6", - "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", - "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", - "@tauri-apps/cli-win32-x64-msvc": "2.9.6" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz", - "integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz", - "integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz", - "integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz", - "integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz", - "integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz", - "integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz", - "integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz", - "integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz", - "integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz", - "integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz", - "integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.2.tgz", - "integrity": "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-fs": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.4.tgz", - "integrity": "sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-notification": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz", - "integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-shell": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.3.tgz", - "integrity": "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", - "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", - "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } - }, - "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.1.tgz", - "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "fast-glob": "^3.3.2", - "fflate": "^0.8.1", - "flatted": "^3.2.9", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "sirv": "^2.0.4" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } - }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cssstyle/node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "24.1.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", - "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.0.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.4", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^2.11.2" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.400.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.400.0.tgz", - "integrity": "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "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 - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-virtuoso": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.17.0.tgz", - "integrity": "sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w==", - "license": "MIT", - "peerDependencies": { - "react": ">=16 || >=17 || >= 18 || >= 19", - "react-dom": ">=16 || >=17 || >= 18 || >=19" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "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.2", - "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.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - } - } -} diff --git a/client/package.json b/client/package.json deleted file mode 100644 index f61479a..0000000 --- a/client/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "noteflow-client", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "tauri": "tauri", - "lint": "biome check src", - "lint:fix": "biome check src --write", - "lint:rs": "cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings", - "format": "prettier --write \"src/**/*.{ts,tsx}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", - "format:rs": "cargo fmt --manifest-path src-tauri/Cargo.toml", - "typecheck": "tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui" - }, - "dependencies": { - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-slider": "^1.1.2", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-dialog": "^2.0.0", - "@tauri-apps/plugin-fs": "^2.0.0", - "@tauri-apps/plugin-notification": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "date-fns": "^3.3.0", - "immer": "^10.0.0", - "lucide-react": "^0.400.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-virtuoso": "^4.7.0", - "tailwind-merge": "^2.2.0", - "zustand": "^4.5.0" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.10", - "@playwright/test": "^1.41.0", - "@tauri-apps/cli": "^2.0.0", - "@testing-library/jest-dom": "^6.4.0", - "@testing-library/react": "^14.2.0", - "@testing-library/user-event": "^14.5.0", - "@types/node": "^25.0.3", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.2.0", - "@vitest/coverage-v8": "^1.2.0", - "@vitest/ui": "^1.2.0", - "autoprefixer": "^10.4.0", - "jsdom": "^24.0.0", - "postcss": "^8.4.0", - "prettier": "^3.7.4", - "tailwindcss": "^3.4.0", - "tailwindcss-animate": "^1.0.7", - "typescript": "^5.3.0", - "vite": "^5.1.0", - "vitest": "^1.2.0" - } -} diff --git a/client/playwright.config.ts b/client/playwright.config.ts deleted file mode 100644 index 103fbd4..0000000 --- a/client/playwright.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * Playwright configuration for NoteFlow Tauri E2E tests - * - * These tests run against the development server and verify - * end-to-end user flows. - */ -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'html', - timeout: 30000, - expect: { - timeout: 5000, - }, - use: { - baseURL: 'http://localhost:1420', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - }, - - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - ], - - webServer: { - command: 'npm run dev', - url: 'http://localhost:1420', - reuseExistingServer: !process.env.CI, - timeout: 120000, - }, -}); diff --git a/client/postcss.config.js b/client/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/client/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/client/src-tauri/Cargo.lock b/client/src-tauri/Cargo.lock deleted file mode 100644 index fcfbbf1..0000000 --- a/client/src-tauri/Cargo.lock +++ /dev/null @@ -1,7648 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "active-win-pos-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e227f8493de9f5e493f8e762ac7516d2ae42464df2e8122fcafd604f0b16c634" -dependencies = [ - "appkit-nsworkspace-bindings", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "objc", - "windows 0.48.0", - "xcb", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.10.0", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "appkit-nsworkspace-bindings" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062382938604cfa02c03689ab75af0e7eb79175ba0d0b2bcfad18f5190702dd7" -dependencies = [ - "bindgen 0.68.1", - "objc", -] - -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "tokio", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus 5.12.0", -] - -[[package]] -name = "async-broadcast" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" -dependencies = [ - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.3.0", - "futures-lite 2.6.1", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "blocking", - "futures-lite 1.13.0", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.6.1", - "parking", - "polling 3.11.0", - "rustix 1.1.2", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.44", - "windows-sys 0.48.0", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io 2.6.0", - "async-lock 3.4.1", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.1", - "futures-lite 2.6.1", - "rustix 1.1.2", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io 2.6.0", - "async-lock 3.4.1", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.2", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.68.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.111", - "which", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.111", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite 2.6.1", - "piper", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.10.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.10+spec-1.1.0", -] - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.2.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.9", -] - -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "libc", -] - -[[package]] -name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen 0.72.1", -] - -[[package]] -name = "cpal" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk 0.8.0", - "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.54.0", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.111", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "dasp" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" -dependencies = [ - "dasp_envelope", - "dasp_frame", - "dasp_interpolate", - "dasp_peak", - "dasp_ring_buffer", - "dasp_rms", - "dasp_sample", - "dasp_signal", - "dasp_slice", - "dasp_window", -] - -[[package]] -name = "dasp_envelope" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" -dependencies = [ - "dasp_frame", - "dasp_peak", - "dasp_ring_buffer", - "dasp_rms", - "dasp_sample", -] - -[[package]] -name = "dasp_frame" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" -dependencies = [ - "dasp_sample", -] - -[[package]] -name = "dasp_interpolate" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" -dependencies = [ - "dasp_frame", - "dasp_ring_buffer", - "dasp_sample", -] - -[[package]] -name = "dasp_peak" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" -dependencies = [ - "dasp_frame", - "dasp_sample", -] - -[[package]] -name = "dasp_ring_buffer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" - -[[package]] -name = "dasp_rms" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" -dependencies = [ - "dasp_frame", - "dasp_ring_buffer", - "dasp_sample", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "dasp_signal" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" -dependencies = [ - "dasp_envelope", - "dasp_frame", - "dasp_interpolate", - "dasp_peak", - "dasp_ring_buffer", - "dasp_rms", - "dasp_sample", - "dasp_window", -] - -[[package]] -name = "dasp_slice" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" -dependencies = [ - "dasp_frame", - "dasp_sample", -] - -[[package]] -name = "dasp_window" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" -dependencies = [ - "dasp_sample", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.111", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading 0.8.9", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embed-resource" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 0.9.10+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset 0.9.1", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fragile" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand 2.3.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.2", - "windows-link 0.2.1", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.10.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "global-hotkey" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" -dependencies = [ - "crossbeam-channel", - "keyboard-types", - "objc2", - "objc2-app-kit", - "once_cell", - "serde", - "thiserror 2.0.17", - "windows-sys 0.59.0", - "x11rb", - "xkeysym", -] - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.12.1", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever", - "match_token", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" -dependencies = [ - "byteorder", - "png", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.10.0", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "keyring" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363387f0019d714aa60cc30ab4fe501a747f4c08fc58f069dd14be971bd495a0" -dependencies = [ - "byteorder", - "lazy_static", - "linux-keyutils", - "secret-service", - "security-framework", - "windows-sys 0.52.0", -] - -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.12.1", - "selectors", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading 0.7.4", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "libredox" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" -dependencies = [ - "bitflags 2.10.0", - "libc", -] - -[[package]] -name = "linux-keyutils" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" -dependencies = [ - "bitflags 2.10.0", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "mac-notification-sys" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" -dependencies = [ - "cc", - "objc2", - "objc2-foundation", - "time", -] - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", -] - -[[package]] -name = "mockall" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "lazy_static", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "muda" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.17", - "windows-sys 0.60.2", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys 0.6.0+11769913", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset 0.9.1", -] - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "noteflow-client" -version = "0.1.0" -dependencies = [ - "active-win-pos-rs", - "aes-gcm", - "anyhow", - "async-stream", - "chrono", - "cocoa", - "cpal", - "dasp", - "directories", - "futures", - "keyring", - "log", - "mockall", - "objc", - "once_cell", - "parking_lot", - "prost", - "prost-types", - "rand 0.8.5", - "ringbuf", - "rodio", - "rubato", - "serde", - "serde_json", - "sysinfo", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-fs", - "tauri-plugin-global-shortcut", - "tauri-plugin-notification", - "tauri-plugin-shell", - "tauri-plugin-window-state", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tokio-test", - "tonic", - "tonic-build", - "tracing", - "tracing-subscriber", - "uuid", - "windows 0.52.0", - "x11rb", -] - -[[package]] -name = "notify-rust" -version = "4.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" -dependencies = [ - "futures-lite 2.6.1", - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus 5.12.0", -] - -[[package]] -name = "ntapi" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.10.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "os_pipe" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.12.1", -] - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.1", -] - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand 2.3.0", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap 2.12.1", - "quick-xml 0.38.4", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.111", -] - -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck 0.5.0", - "itertools 0.14.0", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.111", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 2.0.17", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "reqwest" -version = "0.12.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-util", - "tower 0.5.2", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rfd" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" -dependencies = [ - "ashpd", - "block2", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "ringbuf" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" -dependencies = [ - "crossbeam-utils", - "portable-atomic", - "portable-atomic-util", -] - -[[package]] -name = "rodio" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" -dependencies = [ - "cpal", - "hound", - "thiserror 1.0.69", -] - -[[package]] -name = "rubato" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "realfft", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.111", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "secret-service" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5204d39df37f06d1944935232fd2dfe05008def7ca599bf28c0800366c8a8f9" -dependencies = [ - "aes", - "cbc", - "futures-util", - "generic-array", - "hkdf", - "num", - "once_cell", - "rand 0.8.5", - "serde", - "sha2", - "zbus 3.15.2", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser", - "derive_more", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.12.1", - "schemars 0.9.0", - "schemars 1.1.0", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shared_child" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" -dependencies = [ - "libc", - "sigchld", - "windows-sys 0.60.2", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "sigchld" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" -dependencies = [ - "libc", - "os_pipe", - "signal-hook", -] - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" -dependencies = [ - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk 0.9.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "sysinfo" -version = "0.31.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows 0.57.0", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.34.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" -dependencies = [ - "bitflags 2.10.0", - "block2", - "core-foundation 0.10.1", - "core-graphics 0.24.0", - "crossbeam-channel", - "dispatch", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "lazy_static", - "libc", - "log", - "ndk 0.9.0", - "ndk-context", - "ndk-sys 0.6.0+11769913", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "parking_lot", - "raw-window-handle", - "scopeguard", - "tao-macros", - "unicode-segmentation", - "url", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.17", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows 0.61.3", -] - -[[package]] -name = "tauri-build" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "toml 0.9.10+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.111", - "tauri-utils", - "thiserror 2.0.17", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.111", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.10+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.17", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" -dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.17", - "toml 0.9.10+spec-1.1.0", - "url", -] - -[[package]] -name = "tauri-plugin-global-shortcut" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" -dependencies = [ - "global-hotkey", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.17", -] - -[[package]] -name = "tauri-plugin-notification" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" -dependencies = [ - "log", - "notify-rust", - "rand 0.9.2", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.17", - "time", - "url", -] - -[[package]] -name = "tauri-plugin-shell" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" -dependencies = [ - "encoding_rs", - "log", - "open", - "os_pipe", - "regex", - "schemars 0.8.22", - "serde", - "serde_json", - "shared_child", - "tauri", - "tauri-plugin", - "thiserror 2.0.17", - "tokio", -] - -[[package]] -name = "tauri-plugin-window-state" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" -dependencies = [ - "bitflags 2.10.0", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.17", -] - -[[package]] -name = "tauri-runtime" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.17", - "url", - "webkit2gtk", - "webview2-com", - "windows 0.61.3", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows 0.61.3", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dunce", - "glob", - "html5ever", - "http", - "infer", - "json-patch", - "kuchikiki", - "log", - "memchr", - "phf 0.11.3", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.17", - "toml 0.9.10+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" -dependencies = [ - "dunce", - "embed-resource", - "toml 0.9.10+spec-1.1.0", -] - -[[package]] -name = "tauri-winrt-notification" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" -dependencies = [ - "quick-xml 0.37.5", - "thiserror 2.0.17", - "windows 0.61.3", - "windows-version", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand 2.3.0", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-test" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" -dependencies = [ - "async-stream", - "bytes", - "futures-core", - "tokio", - "tokio-stream", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.10+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" -dependencies = [ - "indexmap 2.12.1", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.14", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.12.1", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.12.1", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap 2.12.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.14", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow 0.7.14", -] - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "socket2 0.5.10", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] -name = "tray-icon" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.17", - "windows-sys 0.60.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset 0.9.1", - "tempfile", - "winapi", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.111", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.2", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" -dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.2", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" -dependencies = [ - "proc-macro2", - "quick-xml 0.37.5", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" -dependencies = [ - "dlib", - "log", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" -dependencies = [ - "thiserror 2.0.17", - "windows 0.61.3", - "windows-core 0.61.2", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wry" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dpi", - "dunce", - "gdkx11", - "gtk", - "html5ever", - "http", - "javascriptcore-rs", - "jni", - "kuchikiki", - "libc", - "ndk 0.9.0", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.17", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "gethostname", - "rustix 1.1.2", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "xcb" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" -dependencies = [ - "bitflags 1.3.2", - "libc", - "quick-xml 0.30.0", -] - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zbus" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" -dependencies = [ - "async-broadcast 0.5.1", - "async-executor", - "async-fs", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-process 1.8.1", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "byteorder", - "derivative", - "enumflags2", - "event-listener 2.5.3", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix 0.26.4", - "once_cell", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "winapi", - "xdg-home", - "zbus_macros 3.15.2", - "zbus_names 2.6.1", - "zvariant 3.15.2", -] - -[[package]] -name = "zbus" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" -dependencies = [ - "async-broadcast 0.7.2", - "async-executor", - "async-io 2.6.0", - "async-lock 3.4.1", - "async-process 2.5.0", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener 5.4.1", - "futures-core", - "futures-lite 2.6.1", - "hex", - "nix 0.30.1", - "ordered-stream", - "serde", - "serde_repr", - "tokio", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 0.7.14", - "zbus_macros 5.12.0", - "zbus_names 4.2.0", - "zvariant 5.8.0", -] - -[[package]] -name = "zbus_macros" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "zvariant_utils 1.0.1", -] - -[[package]] -name = "zbus_macros" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.111", - "zbus_names 4.2.0", - "zvariant 5.8.0", - "zvariant_utils 3.2.1", -] - -[[package]] -name = "zbus_names" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" -dependencies = [ - "serde", - "static_assertions", - "zvariant 3.15.2", -] - -[[package]] -name = "zbus_names" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" -dependencies = [ - "serde", - "static_assertions", - "winnow 0.7.14", - "zvariant 5.8.0", -] - -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zvariant" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "static_assertions", - "zvariant_derive 3.15.2", -] - -[[package]] -name = "zvariant" -version = "5.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow 0.7.14", - "zvariant_derive 5.8.0", - "zvariant_utils 3.2.1", -] - -[[package]] -name = "zvariant_derive" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", - "zvariant_utils 1.0.1", -] - -[[package]] -name = "zvariant_derive" -version = "5.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.111", - "zvariant_utils 3.2.1", -] - -[[package]] -name = "zvariant_utils" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "zvariant_utils" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.111", - "winnow 0.7.14", -] diff --git a/client/src-tauri/Cargo.toml b/client/src-tauri/Cargo.toml deleted file mode 100644 index d46a7fd..0000000 --- a/client/src-tauri/Cargo.toml +++ /dev/null @@ -1,95 +0,0 @@ -[package] -name = "noteflow-client" -version = "0.1.0" -edition = "2021" -rust-version = "1.75" -description = "NoteFlow Meeting Notetaker - Tauri Client" -authors = ["NoteFlow Team"] - -[lib] -name = "noteflow_tauri_lib" -crate-type = ["lib", "cdylib", "staticlib"] - -[build-dependencies] -tauri-build = { version = "2.0", features = [] } -tonic-build = "0.12" - -[dependencies] -# Tauri -tauri = { version = "2.0", features = [] } -tauri-plugin-shell = "2.0" -tauri-plugin-dialog = "2.0" -tauri-plugin-fs = "2.0" -tauri-plugin-notification = "2.0" -tauri-plugin-global-shortcut = "2.0" -tauri-plugin-window-state = "2.0" - -# Async runtime -tokio = { version = "1.35", features = ["full"] } -tokio-stream = "0.1" -futures = "0.3" -async-stream = "0.3" - -# gRPC -tonic = "0.12" -prost = "0.13" -prost-types = "0.13" - -# Audio -cpal = "0.15" -rodio = { version = "0.19", default-features = false, features = ["wav"] } -rubato = "0.15" -ringbuf = "0.4" -dasp = { version = "0.11", features = ["signal", "interpolate"] } - -# Encryption -aes-gcm = "0.10" -rand = "0.8" - -# Keychain -keyring = "2.3" - -# System integration -sysinfo = "0.31" -active-win-pos-rs = "0.8" - -# Serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# Time -chrono = { version = "0.4", features = ["serde"] } - -# Error handling -thiserror = "1.0" -anyhow = "1.0" - -# Logging -log = "0.4" -tracing = "0.1" -tracing-subscriber = "0.3" - -# Utilities -uuid = { version = "1.6", features = ["v4", "serde"] } -parking_lot = "0.12" -once_cell = "1.19" -directories = "5.0" - -[target.'cfg(target_os = "macos")'.dependencies] -cocoa = "0.25" -objc = "0.2" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.52", features = ["Win32_UI_WindowsAndMessaging"] } - -[target.'cfg(target_os = "linux")'.dependencies] -x11rb = "0.13" - -[dev-dependencies] -tokio-test = "0.4" -tempfile = "3.9" -mockall = "0.12" - -[features] -default = ["custom-protocol"] -custom-protocol = ["tauri/custom-protocol"] diff --git a/client/src-tauri/build.rs b/client/src-tauri/build.rs deleted file mode 100644 index b5b1416..0000000 --- a/client/src-tauri/build.rs +++ /dev/null @@ -1,22 +0,0 @@ -// src-tauri/build.rs -use std::path::PathBuf; - -fn main() -> Result<(), Box> { - // Compile protobuf definitions - let proto_file = PathBuf::from("../../src/noteflow/grpc/proto/noteflow.proto"); - let proto_include = PathBuf::from("../../src/noteflow/grpc/proto"); - - // Only compile protos if the file exists - if proto_file.exists() { - tonic_build::configure() - .build_server(false) // Client only - .build_client(true) - .out_dir("src/grpc") - .compile_protos(&[proto_file], &[proto_include])?; - } - - // Standard Tauri build - tauri_build::build(); - - Ok(()) -} diff --git a/client/src-tauri/capabilities/default.json b/client/src-tauri/capabilities/default.json deleted file mode 100644 index 5a40136..0000000 --- a/client/src-tauri/capabilities/default.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2.0.0/capability", - "identifier": "default", - "description": "Default capabilities for NoteFlow", - "windows": ["main"], - "permissions": [ - "core:default", - "shell:allow-open", - "dialog:allow-save", - "dialog:allow-open", - "fs:allow-read", - "fs:allow-write", - "notification:default", - "global-shortcut:allow-register", - "window-state:allow-restore-state", - "window-state:allow-save-window-state" - ] -} diff --git a/client/src-tauri/icons/128x128.png b/client/src-tauri/icons/128x128.png deleted file mode 100644 index f44ac8f..0000000 Binary files a/client/src-tauri/icons/128x128.png and /dev/null differ diff --git a/client/src-tauri/icons/128x128@2x.png b/client/src-tauri/icons/128x128@2x.png deleted file mode 100644 index 6639652..0000000 Binary files a/client/src-tauri/icons/128x128@2x.png and /dev/null differ diff --git a/client/src-tauri/icons/32x32.png b/client/src-tauri/icons/32x32.png deleted file mode 100644 index b32ffba..0000000 Binary files a/client/src-tauri/icons/32x32.png and /dev/null differ diff --git a/client/src-tauri/icons/icon.icns b/client/src-tauri/icons/icon.icns deleted file mode 100644 index e69de29..0000000 diff --git a/client/src-tauri/icons/icon.ico b/client/src-tauri/icons/icon.ico deleted file mode 100644 index e69de29..0000000 diff --git a/client/src-tauri/src/audio/capture.rs b/client/src-tauri/src/audio/capture.rs deleted file mode 100644 index 8ca4f50..0000000 --- a/client/src-tauri/src/audio/capture.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Audio capture using cpal. -//! -//! Provides real-time audio input capture with callback-based sample delivery. - -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{SampleFormat, Stream, StreamConfig}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use crate::constants::audio::DEFAULT_SAMPLE_RATE; -use crate::helpers::now_timestamp; - -/// Audio capture handle. -/// -/// Wraps a cpal input stream and provides start/stop controls. -/// Audio samples are delivered via callback as f32 with timestamp. -pub struct AudioCapture { - stream: Option, - running: Arc, -} - -impl AudioCapture { - /// Create a new audio capture instance. - pub fn new() -> Result { - Ok(Self { - stream: None, - running: Arc::new(AtomicBool::new(false)), - }) - } - - /// Start capturing audio from the default input device. - /// - /// The callback receives audio samples as f32 slice and a timestamp (Unix seconds). - /// Samples are delivered in chunks as they become available from the audio device. - pub fn start(&mut self, callback: F) -> Result<(), String> - where - F: FnMut(&[f32], f64) + Send + 'static, - { - if self.running.load(Ordering::SeqCst) { - return Err("Capture already running".to_string()); - } - - let host = cpal::default_host(); - let device = host - .default_input_device() - .ok_or("No input device available")?; - - let supported_config = device - .default_input_config() - .map_err(|e| format!("Failed to get input config: {e}"))?; - - let sample_format = supported_config.sample_format(); - let config: StreamConfig = supported_config.into(); - - // Log the device info - let device_name = device.name().unwrap_or_else(|_| "Unknown".to_string()); - log::info!( - "Starting audio capture: device={}, rate={}, channels={}, format={:?}", - device_name, - config.sample_rate.0, - config.channels, - sample_format - ); - - let running = Arc::clone(&self.running); - let error_running = Arc::clone(&self.running); - - // Build the stream based on sample format - let stream = match sample_format { - SampleFormat::F32 => self.build_stream(&device, &config, callback, running)?, - SampleFormat::I16 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::U16 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::I8 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::I32 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::I64 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::U8 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::U32 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::U64 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - SampleFormat::F64 => { - self.build_stream_converting::(&device, &config, callback, running)? - } - _ => return Err(format!("Unsupported sample format: {:?}", sample_format)), - }; - - // Start the stream - stream.play().map_err(|e| { - error_running.store(false, Ordering::SeqCst); - format!("Failed to start stream: {e}") - })?; - - self.running.store(true, Ordering::SeqCst); - self.stream = Some(stream); - - Ok(()) - } - - /// Build a stream for f32 samples (native, no conversion needed). - fn build_stream( - &self, - device: &cpal::Device, - config: &StreamConfig, - mut callback: F, - running: Arc, - ) -> Result - where - F: FnMut(&[f32], f64) + Send + 'static, - { - let err_running = Arc::clone(&running); - - device - .build_input_stream( - config, - move |data: &[f32], _: &cpal::InputCallbackInfo| { - if running.load(Ordering::SeqCst) { - let timestamp = now_timestamp(); - callback(data, timestamp); - } - }, - move |err| { - log::error!("Audio stream error: {}", err); - err_running.store(false, Ordering::SeqCst); - }, - None, - ) - .map_err(|e| format!("Failed to build input stream: {e}")) - } - - /// Build a stream with sample format conversion to f32. - fn build_stream_converting( - &self, - device: &cpal::Device, - config: &StreamConfig, - mut callback: F, - running: Arc, - ) -> Result - where - T: cpal::SizedSample + cpal::FromSample + Send + 'static, - f32: cpal::FromSample, - F: FnMut(&[f32], f64) + Send + 'static, - { - let err_running = Arc::clone(&running); - - device - .build_input_stream( - config, - move |data: &[T], _: &cpal::InputCallbackInfo| { - if running.load(Ordering::SeqCst) { - let timestamp = now_timestamp(); - // Convert samples to f32 - let converted: Vec = - data.iter().map(|&s| cpal::Sample::from_sample(s)).collect(); - callback(&converted, timestamp); - } - }, - move |err| { - log::error!("Audio stream error: {}", err); - err_running.store(false, Ordering::SeqCst); - }, - None, - ) - .map_err(|e| format!("Failed to build input stream: {e}")) - } - - /// Stop capturing audio. - pub fn stop(&mut self) { - self.running.store(false, Ordering::SeqCst); - // Dropping the stream will stop it - self.stream = None; - log::info!("Audio capture stopped"); - } - - /// Check if capture is running. - pub fn is_running(&self) -> bool { - self.running.load(Ordering::SeqCst) - } - - /// Get the default sample rate. - /// - /// Returns the configured default or queries the default device. - pub fn default_sample_rate() -> u32 { - cpal::default_host() - .default_input_device() - .and_then(|d| d.default_input_config().ok()) - .map(|c| c.sample_rate().0) - .unwrap_or(DEFAULT_SAMPLE_RATE) - } -} - -impl Default for AudioCapture { - fn default() -> Self { - Self::new().expect("Failed to create AudioCapture") - } -} - -// Stream is not Send, but our wrapper is safe because we only access -// the stream from the thread that created it -unsafe impl Send for AudioCapture {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_create_capture() { - let capture = AudioCapture::new(); - assert!(capture.is_ok()); - } - - #[test] - fn default_sample_rate_is_reasonable() { - let rate = AudioCapture::default_sample_rate(); - // Sample rate should be between 8kHz and 192kHz - assert!(rate >= 8000); - assert!(rate <= 192000); - } - - #[test] - fn capture_starts_not_running() { - let capture = AudioCapture::new().unwrap(); - assert!(!capture.is_running()); - } -} diff --git a/client/src-tauri/src/audio/devices.rs b/client/src-tauri/src/audio/devices.rs deleted file mode 100644 index a3d141f..0000000 --- a/client/src-tauri/src/audio/devices.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Audio device enumeration using cpal. - -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - -use crate::grpc::types::AudioDeviceInfo; -use cpal::traits::{DeviceTrait, HostTrait}; - -/// Generate a stable device ID from device name. -/// -/// Uses hash of name to provide consistent ID across restarts, -/// unlike enumeration index which changes with device ordering. -fn stable_device_id(name: &str) -> u32 { - let mut hasher = DefaultHasher::new(); - name.hash(&mut hasher); - (hasher.finish() % u32::MAX as u64) as u32 -} - -/// List available audio input devices. -pub fn list_input_devices() -> Result, String> { - let host = cpal::default_host(); - let default_name = host - .default_input_device() - .and_then(|d| d.name().ok()) - .unwrap_or_default(); - - let mut devices = Vec::new(); - - for device in host - .input_devices() - .map_err(|e| format!("Failed to enumerate devices: {e}"))? - { - let name = match device.name() { - Ok(n) => n, - Err(_) => continue, - }; - let config = device - .default_input_config() - .map_err(|e| format!("Failed to get config: {e}"))?; - - devices.push(AudioDeviceInfo { - id: stable_device_id(&name), - name: name.clone(), - channels: config.channels() as u32, - sample_rate: config.sample_rate().0, - is_default: name == default_name, - }); - } - - Ok(devices) -} - -/// Get the default input device. -pub fn get_default_input_device() -> Result, String> { - let host = cpal::default_host(); - - let device = match host.default_input_device() { - Some(d) => d, - None => return Ok(None), - }; - - let name = device - .name() - .map_err(|e| format!("Failed to get name: {e}"))?; - let config = device - .default_input_config() - .map_err(|e| format!("Failed to get config: {e}"))?; - - Ok(Some(AudioDeviceInfo { - id: stable_device_id(&name), - name, - channels: config.channels() as u32, - sample_rate: config.sample_rate().0, - is_default: true, - })) -} diff --git a/client/src-tauri/src/audio/loader.rs b/client/src-tauri/src/audio/loader.rs deleted file mode 100644 index 0258272..0000000 --- a/client/src-tauri/src/audio/loader.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Audio file loading and decryption. -//! -//! Decrypts and parses .nfaudio files into playable audio buffers. - -use std::path::Path; - -use crate::crypto::CryptoBox; -use crate::grpc::types::TimestampedAudio; - -/// Audio file format constants -const SAMPLE_RATE_BYTES: usize = 4; -const NUM_SAMPLES_BYTES: usize = 4; -const HEADER_SIZE: usize = SAMPLE_RATE_BYTES + NUM_SAMPLES_BYTES; -const BYTES_PER_SAMPLE: usize = 4; - -/// Default chunk duration in seconds for TimestampedAudio segments -const CHUNK_DURATION: f64 = 0.1; - -/// Load and decrypt a .nfaudio file into playable audio buffer. -/// -/// File format (after decryption): -/// - [4 bytes: sample_rate u32 LE] -/// - [4 bytes: num_samples u32 LE] -/// - [num_samples * 4 bytes: f32 samples LE] -pub fn load_audio_file( - crypto: &CryptoBox, - path: &Path, -) -> Result<(Vec, u32), String> { - // Read encrypted file - let encrypted = std::fs::read(path).map_err(|e| format!("Failed to read audio file: {}", e))?; - - // Decrypt - let decrypted = crypto.decrypt(&encrypted)?; - - // Parse header - if decrypted.len() < HEADER_SIZE { - return Err("Audio file too short".to_string()); - } - - let sample_rate = u32::from_le_bytes( - decrypted[..SAMPLE_RATE_BYTES] - .try_into() - .map_err(|_| "Invalid sample rate bytes")?, - ); - if sample_rate == 0 { - return Err("Invalid sample rate: 0".to_string()); - } - - let num_samples = u32::from_le_bytes( - decrypted[SAMPLE_RATE_BYTES..HEADER_SIZE] - .try_into() - .map_err(|_| "Invalid sample count bytes")?, - ) as usize; - - // Validate payload size - let expected_size = HEADER_SIZE + num_samples * BYTES_PER_SAMPLE; - if decrypted.len() < expected_size { - return Err(format!( - "Audio file truncated: expected {} bytes, got {}", - expected_size, - decrypted.len() - )); - } - - // Parse samples - let samples = parse_samples(&decrypted[HEADER_SIZE..], num_samples)?; - - // Convert to TimestampedAudio chunks - let buffer = samples_to_chunks(&samples, sample_rate); - - Ok((buffer, sample_rate)) -} - -/// Parse f32 samples from raw bytes. -fn parse_samples(data: &[u8], num_samples: usize) -> Result, String> { - let mut samples = Vec::with_capacity(num_samples); - - for i in 0..num_samples { - let offset = i * BYTES_PER_SAMPLE; - let bytes: [u8; 4] = data[offset..offset + BYTES_PER_SAMPLE] - .try_into() - .map_err(|_| "Invalid sample bytes")?; - samples.push(f32::from_le_bytes(bytes)); - } - - Ok(samples) -} - -/// Convert flat samples into TimestampedAudio chunks. -fn samples_to_chunks(samples: &[f32], sample_rate: u32) -> Vec { - let chunk_samples = ((sample_rate as f64 * CHUNK_DURATION) as usize).max(1); - let mut chunks = Vec::new(); - let mut offset = 0; - - while offset < samples.len() { - let end = (offset + chunk_samples).min(samples.len()); - let frame_count = end - offset; - let duration = frame_count as f64 / sample_rate as f64; - let timestamp = offset as f64 / sample_rate as f64; - - chunks.push(TimestampedAudio { - frames: samples[offset..end].to_vec(), - timestamp, - duration, - }); - - offset = end; - } - - chunks -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_samples_to_chunks() { - let samples: Vec = (0..4800).map(|i| i as f32 / 4800.0).collect(); - let chunks = samples_to_chunks(&samples, 48000); - - assert!(!chunks.is_empty()); - assert!((chunks[0].duration - 0.1).abs() < 0.01); - assert_eq!(chunks[0].timestamp, 0.0); - } -} diff --git a/client/src-tauri/src/audio/mod.rs b/client/src-tauri/src/audio/mod.rs deleted file mode 100644 index 001c166..0000000 --- a/client/src-tauri/src/audio/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Audio capture and playback functionality. -//! -//! Organized into submodules: -//! - `capture`: Audio input using cpal -//! - `playback`: Audio output using rodio -//! - `devices`: Device enumeration -//! - `loader`: Encrypted audio file loading - -mod capture; -mod devices; -mod loader; -mod playback; - -pub use capture::AudioCapture; -pub use devices::{get_default_input_device, list_input_devices}; -pub use loader::load_audio_file; -pub use playback::{PlaybackHandle, PlaybackStarted}; diff --git a/client/src-tauri/src/audio/playback.rs b/client/src-tauri/src/audio/playback.rs deleted file mode 100644 index e882be8..0000000 --- a/client/src-tauri/src/audio/playback.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! Audio playback using rodio with thread-safe channel-based control. -//! -//! The audio stream is owned by a dedicated thread since `cpal::Stream` is not `Send`. -//! Commands are sent via channels, making `PlaybackHandle` safe to store in `AppState`. - -use crate::grpc::types::TimestampedAudio; -use parking_lot::Mutex; -use rodio::{buffer::SamplesBuffer, OutputStream, Sink}; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::mpsc::{self, Receiver, Sender}; -use std::sync::Arc; -use std::thread::{self, JoinHandle}; - -/// Commands sent to the audio thread. -#[derive(Debug)] -pub enum PlaybackCommand { - /// Start playback with audio buffer and sample rate. - Play(Vec, u32), - /// Pause playback. - Pause, - /// Resume playback. - Resume, - /// Stop playback and reset. - Stop, - /// Shutdown the audio thread. - Shutdown, -} - -/// Response from audio thread after Play command. -#[derive(Debug)] -pub struct PlaybackStarted { - pub duration: f64, - pub playing_flag: Arc, - pub position_atomic: Arc, -} - -/// Thread-safe handle for controlling audio playback. -/// This can be stored in `AppState` as it only contains `Send + Sync` types. -pub struct PlaybackHandle { - command_tx: Sender, - response_rx: Mutex>>, - _thread: JoinHandle<()>, -} - -impl PlaybackHandle { - /// Create a new playback handle, spawning the audio thread. - pub fn new() -> Result { - let (command_tx, command_rx) = mpsc::channel::(); - let (response_tx, response_rx) = mpsc::channel::>(); - - let thread = thread::spawn(move || { - audio_thread_main(command_rx, response_tx); - }); - - Ok(Self { - command_tx, - response_rx: Mutex::new(response_rx), - _thread: thread, - }) - } - - /// Start playback with the given audio buffer. - pub fn play( - &self, - audio_buffer: Vec, - sample_rate: u32, - ) -> Result { - self.command_tx - .send(PlaybackCommand::Play(audio_buffer, sample_rate)) - .map_err(|_| "Audio thread disconnected".to_string())?; - - self.response_rx - .lock() - .recv() - .map_err(|_| "Audio thread disconnected".to_string())? - } - - /// Pause playback. - pub fn pause(&self) -> Result<(), String> { - self.command_tx - .send(PlaybackCommand::Pause) - .map_err(|_| "Audio thread disconnected".to_string()) - } - - /// Resume playback. - pub fn resume(&self) -> Result<(), String> { - self.command_tx - .send(PlaybackCommand::Resume) - .map_err(|_| "Audio thread disconnected".to_string()) - } - - /// Stop playback. - pub fn stop(&self) -> Result<(), String> { - self.command_tx - .send(PlaybackCommand::Stop) - .map_err(|_| "Audio thread disconnected".to_string()) - } -} - -impl Drop for PlaybackHandle { - fn drop(&mut self) { - let _ = self.command_tx.send(PlaybackCommand::Shutdown); - } -} - -// ============================================================================ -// Audio Thread -// ============================================================================ - -/// Main loop for the audio thread. -fn audio_thread_main( - command_rx: Receiver, - response_tx: Sender>, -) { - // Audio state owned by this thread (not Send, stays here) - let mut audio_state: Option = None; - - loop { - let command = match command_rx.recv() { - Ok(cmd) => cmd, - Err(_) => break, // Channel closed - }; - - match command { - PlaybackCommand::Play(audio_buffer, sample_rate) => { - let result = start_playback(&mut audio_state, audio_buffer, sample_rate); - let _ = response_tx.send(result); - } - PlaybackCommand::Pause => { - if let Some(ref state) = audio_state { - state.sink.pause(); - state.playing.store(false, Ordering::SeqCst); - } - } - PlaybackCommand::Resume => { - if let Some(ref state) = audio_state { - state.sink.play(); - state.playing.store(true, Ordering::SeqCst); - } - } - PlaybackCommand::Stop => { - if let Some(ref state) = audio_state { - state.sink.stop(); - state.playing.store(false, Ordering::SeqCst); - state.position.store(0, Ordering::SeqCst); - } - audio_state = None; - } - PlaybackCommand::Shutdown => break, - } - } -} - -/// Internal audio state owned by the audio thread. -struct AudioState { - _stream: OutputStream, - sink: Sink, - position: Arc, - playing: Arc, -} - -fn start_playback( - audio_state: &mut Option, - audio_buffer: Vec, - sample_rate: u32, -) -> Result { - if audio_buffer.is_empty() { - return Err("No audio to play".to_string()); - } - - let sample_rate = sample_rate.max(1); - - // Create output stream - let (stream, stream_handle) = - OutputStream::try_default().map_err(|e| format!("Failed to get output stream: {e}"))?; - - let sink = - Sink::try_new(&stream_handle).map_err(|e| format!("Failed to create sink: {e}"))?; - - // Concatenate all frames into single buffer - let samples: Vec = audio_buffer - .iter() - .flat_map(|ta| ta.frames.iter().copied()) - .collect(); - - let duration = samples.len() as f64 / sample_rate as f64; - - let source = SamplesBuffer::new(1, sample_rate, samples); - sink.append(source); - sink.play(); - - let position = Arc::new(AtomicU64::new(0)); - let playing = Arc::new(AtomicBool::new(true)); - - let started = PlaybackStarted { - duration, - playing_flag: Arc::clone(&playing), - position_atomic: Arc::clone(&position), - }; - - *audio_state = Some(AudioState { - _stream: stream, - sink, - position, - playing, - }); - - Ok(started) -} diff --git a/client/src-tauri/src/cache/memory.rs b/client/src-tauri/src/cache/memory.rs deleted file mode 100644 index d27b0b8..0000000 --- a/client/src-tauri/src/cache/memory.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! In-memory cache implementation -//! -//! A thread-safe, LRU-based in-memory cache with TTL support. - -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, Instant}; - -use parking_lot::RwLock; -use serde::{de::DeserializeOwned, Serialize}; - -use super::{Cache, CacheError, CacheResult, CacheStats}; - -/// Cached entry with metadata -#[derive(Debug, Clone)] -struct CacheEntry { - /// Serialized value as JSON - data: String, - /// When this entry was created - created_at: Instant, - /// Time-to-live duration - ttl: Duration, - /// Last access time for LRU eviction - last_access: Instant, - /// Approximate size in bytes - size: usize, -} - -impl CacheEntry { - fn new(data: String, ttl: Duration) -> Self { - let size = data.len(); - let now = Instant::now(); - Self { - data, - created_at: now, - ttl, - last_access: now, - size, - } - } - - fn is_expired(&self) -> bool { - self.created_at.elapsed() > self.ttl - } - - fn touch(&mut self) { - self.last_access = Instant::now(); - } -} - -/// Thread-safe in-memory cache with LRU eviction -#[derive(Debug)] -pub struct MemoryCache { - /// The cache storage - entries: RwLock>, - /// Maximum number of entries - max_items: usize, - /// Default TTL for entries - default_ttl: Duration, - /// Hit counter - hits: AtomicU64, - /// Miss counter - misses: AtomicU64, -} - -impl MemoryCache { - /// Create a new memory cache - pub fn new(max_items: usize, default_ttl: Duration) -> Self { - Self { - entries: RwLock::new(HashMap::new()), - max_items, - default_ttl, - hits: AtomicU64::new(0), - misses: AtomicU64::new(0), - } - } - - /// Evict expired entries (operates on already-locked entries) - fn evict_expired_locked(entries: &mut HashMap) { - entries.retain(|_, entry| !entry.is_expired()); - } - - /// Evict least recently used entries if over capacity (operates on already-locked entries) - fn evict_lru_locked(entries: &mut HashMap, max_items: usize) { - while entries.len() > max_items { - // Find the least recently accessed entry - if let Some(lru_key) = entries - .iter() - .min_by_key(|(_, entry)| entry.last_access) - .map(|(key, _)| key.clone()) - { - entries.remove(&lru_key); - } else { - break; - } - } - } -} - -impl Cache for MemoryCache { - fn get( - &self, - key: &str, - ) -> Pin>> + Send + '_>> { - let key = key.to_string(); - Box::pin(async move { - // Use write lock directly to avoid TOCTOU race condition - // between checking expiry and updating access time - let mut entries = self.entries.write(); - - if let Some(entry) = entries.get_mut(&key) { - if entry.is_expired() { - entries.remove(&key); - self.misses.fetch_add(1, Ordering::Relaxed); - return Ok(None); - } - - // Clone data before releasing lock to avoid holding lock during deserialization - let data = entry.data.clone(); - entry.touch(); - drop(entries); // Release lock before CPU-intensive deserialization - - self.hits.fetch_add(1, Ordering::Relaxed); - - let value: T = serde_json::from_str(&data) - .map_err(|e| CacheError::Serialization(e.to_string()))?; - return Ok(Some(value)); - } - - drop(entries); - self.misses.fetch_add(1, Ordering::Relaxed); - Ok(None) - }) - } - - fn set( - &self, - key: &str, - value: &T, - ttl: Option, - ) -> Pin> + Send + '_>> { - let key = key.to_string(); - let ttl = ttl.unwrap_or(self.default_ttl); - let max_items = self.max_items; - - // Serialize synchronously since we have a reference - let data = match serde_json::to_string(value) { - Ok(d) => d, - Err(e) => { - return Box::pin(async move { Err(CacheError::Serialization(e.to_string())) }) - } - }; - - Box::pin(async move { - let entry = CacheEntry::new(data, ttl); - - // Single lock acquisition for all operations to avoid race conditions - let mut entries = self.entries.write(); - - // Evict expired entries - Self::evict_expired_locked(&mut entries); - - // Insert new entry - entries.insert(key, entry); - - // Evict LRU if over capacity - Self::evict_lru_locked(&mut entries, max_items); - - Ok(()) - }) - } - - fn delete(&self, key: &str) -> Pin> + Send + '_>> { - let key = key.to_string(); - Box::pin(async move { - let removed = self.entries.write().remove(&key).is_some(); - Ok(removed) - }) - } - - fn delete_by_prefix( - &self, - prefix: &str, - ) -> Pin> + Send + '_>> { - let prefix = prefix.to_string(); - Box::pin(async move { - let mut entries = self.entries.write(); - let keys_to_remove: Vec = entries - .keys() - .filter(|k| k.starts_with(&prefix)) - .cloned() - .collect(); - let count = keys_to_remove.len(); - for key in keys_to_remove { - entries.remove(&key); - } - Ok(count) - }) - } - - fn exists(&self, key: &str) -> Pin> + Send + '_>> { - let key = key.to_string(); - Box::pin(async move { - let mut entries = self.entries.write(); - match entries.get_mut(&key) { - Some(entry) if entry.is_expired() => { - // Clean up expired entry to prevent memory leak - entries.remove(&key); - self.misses.fetch_add(1, Ordering::Relaxed); - Ok(false) - } - Some(entry) => { - entry.touch(); - self.hits.fetch_add(1, Ordering::Relaxed); - Ok(true) - } - None => { - self.misses.fetch_add(1, Ordering::Relaxed); - Ok(false) - } - } - }) - } - - fn clear(&self) -> Pin> + Send + '_>> { - Box::pin(async move { - self.entries.write().clear(); - self.hits.store(0, Ordering::Relaxed); - self.misses.store(0, Ordering::Relaxed); - Ok(()) - }) - } - - fn stats(&self) -> CacheStats { - let entries = self.entries.read(); - let bytes = entries.values().map(|e| e.size).sum(); - - CacheStats { - hits: self.hits.load(Ordering::Relaxed), - misses: self.misses.load(Ordering::Relaxed), - items: entries.len(), - bytes, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn set_and_get() { - let cache = MemoryCache::new(100, Duration::from_secs(60)); - - cache - .set("key1", &"value1".to_string(), None) - .await - .unwrap(); - let result: Option = cache.get("key1").await.unwrap(); - - assert_eq!(result, Some("value1".to_string())); - } - - #[tokio::test] - async fn get_nonexistent_returns_none() { - let cache = MemoryCache::new(100, Duration::from_secs(60)); - - let result: Option = cache.get("nonexistent").await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn expired_entries_are_removed() { - let cache = MemoryCache::new(100, Duration::from_millis(10)); - - cache - .set("key1", &"value1".to_string(), None) - .await - .unwrap(); - - // Wait for expiration - tokio::time::sleep(Duration::from_millis(20)).await; - - let result: Option = cache.get("key1").await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn delete_removes_entry() { - let cache = MemoryCache::new(100, Duration::from_secs(60)); - - cache - .set("key1", &"value1".to_string(), None) - .await - .unwrap(); - let deleted = cache.delete("key1").await.unwrap(); - - assert!(deleted); - let result: Option = cache.get("key1").await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn exists_checks_presence() { - let cache = MemoryCache::new(100, Duration::from_secs(60)); - - assert!(!cache.exists("key1").await.unwrap()); - - cache - .set("key1", &"value1".to_string(), None) - .await - .unwrap(); - assert!(cache.exists("key1").await.unwrap()); - } - - #[tokio::test] - async fn lru_eviction() { - let cache = MemoryCache::new(2, Duration::from_secs(60)); - - cache - .set("key1", &"value1".to_string(), None) - .await - .unwrap(); - cache - .set("key2", &"value2".to_string(), None) - .await - .unwrap(); - - // Access key1 to make it more recent - let _: Option = cache.get("key1").await.unwrap(); - - // Add key3, should evict key2 (least recently used) - cache - .set("key3", &"value3".to_string(), None) - .await - .unwrap(); - - assert!(cache.exists("key1").await.unwrap()); - assert!(!cache.exists("key2").await.unwrap()); - assert!(cache.exists("key3").await.unwrap()); - } - - #[tokio::test] - async fn stats_tracking() { - let cache = MemoryCache::new(100, Duration::from_secs(60)); - - cache - .set("key1", &"value1".to_string(), None) - .await - .unwrap(); - let _: Option = cache.get("key1").await.unwrap(); // hit - let _: Option = cache.get("key2").await.unwrap(); // miss - - let stats = cache.stats(); - assert_eq!(stats.hits, 1); - assert_eq!(stats.misses, 1); - assert_eq!(stats.items, 1); - } - - #[tokio::test] - async fn complex_types() { - #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] - struct User { - id: u32, - name: String, - } - - let cache = MemoryCache::new(100, Duration::from_secs(60)); - let user = User { - id: 1, - name: "Alice".to_string(), - }; - - cache.set("user:1", &user, None).await.unwrap(); - let result: Option = cache.get("user:1").await.unwrap(); - - assert_eq!(result, Some(user)); - } -} diff --git a/client/src-tauri/src/cache/mod.rs b/client/src-tauri/src/cache/mod.rs deleted file mode 100644 index bace97e..0000000 --- a/client/src-tauri/src/cache/mod.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! Cache abstraction layer -//! -//! Provides a unified caching interface with support for multiple backends: -//! - In-memory cache (default) -//! - Redis cache (requires feature flag) -//! - No-op cache (disabled) - -mod memory; - -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::time::Duration; - -use serde::{de::DeserializeOwned, Serialize}; - -use crate::config::config; - -pub use memory::MemoryCache; - -/// Type alias for async cache results -pub type CacheResult = Result; - -/// Cache error types -#[derive(Debug, Clone, thiserror::Error)] -pub enum CacheError { - #[error("Key not found: {0}")] - NotFound(String), - #[error("Serialization error: {0}")] - Serialization(String), - #[error("Connection error: {0}")] - Connection(String), - #[error("Cache disabled")] - Disabled, -} - -/// Cache trait for all backends -pub trait Cache: Send + Sync { - /// Get a value by key - fn get( - &self, - key: &str, - ) -> Pin>> + Send + '_>>; - - /// Set a value with optional TTL - fn set( - &self, - key: &str, - value: &T, - ttl: Option, - ) -> Pin> + Send + '_>>; - - /// Delete a value by key - fn delete(&self, key: &str) -> Pin> + Send + '_>>; - - /// Delete all values with keys matching the given prefix - fn delete_by_prefix( - &self, - prefix: &str, - ) -> Pin> + Send + '_>>; - - /// Check if a key exists - fn exists(&self, key: &str) -> Pin> + Send + '_>>; - - /// Clear all cached values - fn clear(&self) -> Pin> + Send + '_>>; - - /// Get cache statistics - fn stats(&self) -> CacheStats; -} - -/// Cache statistics -#[derive(Debug, Clone, Default)] -pub struct CacheStats { - /// Number of cache hits - pub hits: u64, - /// Number of cache misses - pub misses: u64, - /// Current number of items - pub items: usize, - /// Total bytes used (approximate) - pub bytes: usize, -} - -impl CacheStats { - /// Hit rate as a percentage - pub fn hit_rate(&self) -> f64 { - let total = self.hits + self.misses; - if total == 0 { - 0.0 - } else { - (self.hits as f64 / total as f64) * 100.0 - } - } -} - -/// No-op cache implementation (when caching is disabled) -#[derive(Debug, Default)] -pub struct NoOpCache; - -impl Cache for NoOpCache { - fn get( - &self, - _key: &str, - ) -> Pin>> + Send + '_>> { - Box::pin(async { Ok(None) }) - } - - fn set( - &self, - _key: &str, - _value: &T, - _ttl: Option, - ) -> Pin> + Send + '_>> { - Box::pin(async { Ok(()) }) - } - - fn delete(&self, _key: &str) -> Pin> + Send + '_>> { - Box::pin(async { Ok(false) }) - } - - fn delete_by_prefix( - &self, - _prefix: &str, - ) -> Pin> + Send + '_>> { - Box::pin(async { Ok(0) }) - } - - fn exists(&self, _key: &str) -> Pin> + Send + '_>> { - Box::pin(async { Ok(false) }) - } - - fn clear(&self) -> Pin> + Send + '_>> { - Box::pin(async { Ok(()) }) - } - - fn stats(&self) -> CacheStats { - CacheStats::default() - } -} - -/// Create a memory cache instance based on configuration -pub fn create_cache() -> Arc { - let cfg = config(); - Arc::new(MemoryCache::new( - cfg.cache.max_memory_items, - Duration::from_secs(cfg.cache.default_ttl_secs), - )) -} - -/// Cache key builder for consistent key formatting -pub struct CacheKey; - -impl CacheKey { - /// Build a key for meeting data - pub fn meeting(meeting_id: &str) -> String { - format!("meeting:{}", meeting_id) - } - - /// Build a key for meeting list - pub fn meeting_list() -> String { - "meetings:list".to_string() - } - - /// Build a key for segments - pub fn segments(meeting_id: &str) -> String { - format!("segments:{}", meeting_id) - } - - /// Build a key for summary - pub fn summary(meeting_id: &str) -> String { - format!("summary:{}", meeting_id) - } - - /// Build a key for server info - pub fn server_info() -> String { - "server:info".to_string() - } - - /// Build a custom key with prefix - pub fn custom(prefix: &str, id: &str) -> String { - format!("{}:{}", prefix, id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn noop_cache_returns_none() { - let cache = NoOpCache; - let result: Option = cache.get("test").await.unwrap(); - assert!(result.is_none()); - } - - #[test] - fn cache_key_formatting() { - assert_eq!(CacheKey::meeting("abc"), "meeting:abc"); - assert_eq!(CacheKey::segments("abc"), "segments:abc"); - assert_eq!(CacheKey::custom("user", "123"), "user:123"); - } - - #[test] - fn cache_stats_hit_rate() { - let stats = CacheStats { - hits: 80, - misses: 20, - items: 100, - bytes: 0, - }; - assert!((stats.hit_rate() - 80.0).abs() < 0.01); - } -} diff --git a/client/src-tauri/src/commands/annotation.rs b/client/src-tauri/src/commands/annotation.rs deleted file mode 100644 index fd4e18d..0000000 --- a/client/src-tauri/src/commands/annotation.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Annotation-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::grpc::types::{AnnotationInfo, AnnotationType}; -use crate::state::AppState; - -/// Add annotation to a meeting -#[tauri::command] -pub async fn add_annotation( - state: State<'_, Arc>, - meeting_id: String, - annotation_type: String, - text: String, - start_time: f64, - end_time: f64, - segment_ids: Option>, -) -> Result { - let anno_type = AnnotationType::from(annotation_type.as_str()); - - let annotation = state - .grpc_client - .add_annotation( - &meeting_id, - anno_type, - &text, - start_time, - end_time, - segment_ids, - ) - .await - .map_err(|e| e.to_string())?; - - // Add to cache - state.annotations.write().push(annotation.clone()); - - Ok(annotation) -} - -/// Get annotation by ID -#[tauri::command] -pub async fn get_annotation( - state: State<'_, Arc>, - annotation_id: String, -) -> Result { - state - .grpc_client - .get_annotation(&annotation_id) - .await - .map_err(|e| e.to_string()) -} - -/// List annotations for a meeting -#[tauri::command] -pub async fn list_annotations( - state: State<'_, Arc>, - meeting_id: String, - start_time: Option, - end_time: Option, -) -> Result, String> { - let annotations = state - .grpc_client - .list_annotations( - &meeting_id, - start_time.unwrap_or(0.0), - end_time.unwrap_or(f64::MAX), - ) - .await - .map_err(|e| e.to_string())?; - - // Update cache - *state.annotations.write() = annotations.clone(); - - Ok(annotations) -} - -/// Update an existing annotation -#[tauri::command] -pub async fn update_annotation( - state: State<'_, Arc>, - annotation_id: String, - annotation_type: Option, - text: Option, - start_time: Option, - end_time: Option, - segment_ids: Option>, -) -> Result { - let anno_type = annotation_type.map(|t| AnnotationType::from(t.as_str())); - - let updated = state - .grpc_client - .update_annotation( - &annotation_id, - anno_type, - text.as_deref(), - start_time, - end_time, - segment_ids, - ) - .await - .map_err(|e| e.to_string())?; - - // Update cache - if let Some(pos) = state - .annotations - .read() - .iter() - .position(|a| a.id == annotation_id) - { - state.annotations.write()[pos] = updated.clone(); - } - - Ok(updated) -} - -/// Delete an annotation -#[tauri::command] -pub async fn delete_annotation( - state: State<'_, Arc>, - annotation_id: String, -) -> Result { - let success = state - .grpc_client - .delete_annotation(&annotation_id) - .await - .map_err(|e| e.to_string())?; - - if success { - state.annotations.write().retain(|a| a.id != annotation_id); - } - - Ok(success) -} diff --git a/client/src-tauri/src/commands/audio.rs b/client/src-tauri/src/commands/audio.rs deleted file mode 100644 index 7dcaae1..0000000 --- a/client/src-tauri/src/commands/audio.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Audio device-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::audio; -use crate::grpc::types::AudioDeviceInfo; -use crate::state::AppState; - -/// List available audio input devices -#[tauri::command] -pub async fn list_audio_devices() -> Result, String> { - audio::list_input_devices() -} - -/// Select an audio input device -#[tauri::command] -pub async fn select_audio_device( - state: State<'_, Arc>, - device_id: Option, -) -> Result<(), String> { - let mut prefs = state.preferences.write(); - - if let Some(id) = device_id { - let devices = audio::list_input_devices()?; - let device_name = devices.iter().find(|d| d.id == id).map(|d| d.name.clone()); - - prefs.insert("audio_device_id".to_string(), serde_json::json!(id)); - prefs.insert( - "audio_device_name".to_string(), - serde_json::json!(device_name), - ); - } else { - prefs.remove("audio_device_id"); - prefs.remove("audio_device_name"); - } - Ok(()) -} - -/// Get current audio device -#[tauri::command] -pub async fn get_current_device( - state: State<'_, Arc>, -) -> Result, String> { - let prefs = state.preferences.read(); - let device_id = prefs - .get("audio_device_id") - .and_then(|v| v.as_u64()) - .map(|id| id as u32); - let device_name = prefs - .get("audio_device_name") - .and_then(|v| v.as_str()) - .map(|name| name.to_string()); - - if device_id.is_none() && device_name.is_none() { - return audio::get_default_input_device(); - } - - let devices = audio::list_input_devices()?; - - if let Some(name) = device_name { - if let Some(device) = devices.iter().find(|d| d.name == name) { - return Ok(Some(device.clone())); - } - } - - if let Some(id) = device_id { - return Ok(devices.into_iter().find(|d| d.id == id)); - } - - audio::get_default_input_device() -} diff --git a/client/src-tauri/src/commands/connection.rs b/client/src-tauri/src/commands/connection.rs deleted file mode 100644 index 37dfb84..0000000 --- a/client/src-tauri/src/commands/connection.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Connection-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::grpc::types::ServerInfo; -use crate::state::{AppState, AppStatus}; - -/// Connect to gRPC server -#[tauri::command] -pub async fn connect( - state: State<'_, Arc>, - address: String, -) -> Result { - // Connect via gRPC client (it manages its own state atomically) - let info = state.grpc_client.connect(&address).await?; - - // Cache server info for quick access (gRPC client is source of truth for connection) - *state.server_info.write() = Some(info.clone()); - - Ok(info) -} - -/// Disconnect from server -#[tauri::command] -pub async fn disconnect(state: State<'_, Arc>) -> Result<(), String> { - // Stop recording if active (check from local state for speed) - if *state.recording.read() { - state.grpc_client.stop_streaming().await; - state.reset_recording_state(); - } - - // Disconnect gRPC (it manages its own state atomically) - state.grpc_client.disconnect().await; - - // Clear cached server info - *state.server_info.write() = None; - - Ok(()) -} - -/// Get current server info -#[tauri::command] -pub async fn get_server_info( - state: State<'_, Arc>, -) -> Result, String> { - Ok(state.server_info.read().clone()) -} - -/// Get full application status -#[tauri::command] -pub async fn get_status(state: State<'_, Arc>) -> Result { - Ok(state.get_status()) -} diff --git a/client/src-tauri/src/commands/diarization.rs b/client/src-tauri/src/commands/diarization.rs deleted file mode 100644 index 552ccd9..0000000 --- a/client/src-tauri/src/commands/diarization.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Diarization-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::grpc::types::{DiarizationResult, RenameSpeakerResult}; -use crate::state::AppState; - -/// Start background speaker diarization refinement -#[tauri::command] -pub async fn refine_speaker_diarization( - state: State<'_, Arc>, - meeting_id: String, - num_speakers: Option, -) -> Result { - state - .grpc_client - .refine_speaker_diarization(&meeting_id, num_speakers) - .await - .map_err(|e| e.to_string()) -} - -/// Get status of a diarization job -#[tauri::command] -pub async fn get_diarization_job_status( - state: State<'_, Arc>, - job_id: String, -) -> Result { - state - .grpc_client - .get_diarization_job_status(&job_id) - .await - .map_err(|e| e.to_string()) -} - -/// Rename a speaker across all segments -#[tauri::command] -pub async fn rename_speaker( - state: State<'_, Arc>, - meeting_id: String, - old_speaker_id: String, - new_speaker_name: String, -) -> Result { - let result = state - .grpc_client - .rename_speaker(&meeting_id, &old_speaker_id, &new_speaker_name) - .await - .map_err(|e| e.to_string())?; - - // Update cached segments - if result.success { - for segment in state.transcript_segments.write().iter_mut() { - if segment.speaker_id == old_speaker_id { - segment.speaker_id = new_speaker_name.clone(); - } - } - } - - Ok(result) -} diff --git a/client/src-tauri/src/commands/export.rs b/client/src-tauri/src/commands/export.rs deleted file mode 100644 index 20bbf7d..0000000 --- a/client/src-tauri/src/commands/export.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Export-related Tauri commands - -use std::sync::Arc; -use tauri::{AppHandle, State}; -use tauri_plugin_dialog::DialogExt; - -use crate::grpc::types::{ExportFormat, ExportResult}; -use crate::state::AppState; - -/// Export meeting transcript -#[tauri::command] -pub async fn export_transcript( - state: State<'_, Arc>, - meeting_id: String, - format: String, -) -> Result { - let fmt = ExportFormat::from(format.as_str()); - state - .grpc_client - .export_transcript(&meeting_id, fmt) - .await - .map_err(|e| e.to_string()) -} - -/// Save export file to disk -#[tauri::command] -pub async fn save_export_file( - app: AppHandle, - content: String, - default_name: String, - extension: String, -) -> Result { - tauri::async_runtime::spawn_blocking(move || { - // Sanitize inputs to prevent path injection - let default_name = default_name.replace(['/', '\\'], "_"); - let extension = extension.trim().trim_start_matches('.').to_string(); - - let file_path = app - .dialog() - .file() - .set_file_name(&default_name) - .add_filter("Export", &[extension.as_str()]) - .blocking_save_file(); - - if let Some(path) = file_path { - std::fs::write(path.as_path().unwrap(), content) - .map_err(|e| format!("Failed to write file: {}", e))?; - Ok(true) - } else { - Ok(false) - } - }) - .await - .map_err(|e| format!("Failed to save file: {}", e))? -} diff --git a/client/src-tauri/src/commands/meeting.rs b/client/src-tauri/src/commands/meeting.rs deleted file mode 100644 index 280dd4e..0000000 --- a/client/src-tauri/src/commands/meeting.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Meeting-related Tauri commands - -use std::sync::Arc; -use tauri::{AppHandle, Emitter, State}; - -use crate::audio::load_audio_file; -use crate::constants::audio::DEFAULT_SAMPLE_RATE; -use crate::constants::Events; -use crate::grpc::types::{MeetingDetails, MeetingInfo, MeetingState}; -use crate::state::AppState; - -/// List meetings with optional filtering -#[tauri::command] -pub async fn list_meetings( - state: State<'_, Arc>, - states: Option>, - limit: Option, - offset: Option, - sort_desc: Option, -) -> Result<(Vec, u32), String> { - let states: Option> = states.map(|s| { - s.iter() - .filter_map(|st| match st.as_str() { - "created" => Some(MeetingState::Created), - "recording" => Some(MeetingState::Recording), - "stopping" => Some(MeetingState::Stopping), - "stopped" => Some(MeetingState::Stopped), - "completed" => Some(MeetingState::Completed), - "error" => Some(MeetingState::Error), - _ => None, - }) - .collect() - }); - - let (meetings, total) = state - .grpc_client - .list_meetings( - states, - limit.unwrap_or(100), - offset.unwrap_or(0), - sort_desc.unwrap_or(true), - ) - .await?; - - // Cache meetings - *state.meetings.write() = meetings.clone(); - - Ok((meetings, total)) -} - -/// Get meeting details with segments and summary -#[tauri::command] -pub async fn get_meeting( - state: State<'_, Arc>, - meeting_id: String, - include_segments: bool, - include_summary: bool, -) -> Result { - state - .grpc_client - .get_meeting(&meeting_id, include_segments, include_summary) - .await - .map_err(|e| e.to_string()) -} - -/// Delete a meeting -#[tauri::command] -pub async fn delete_meeting( - state: State<'_, Arc>, - meeting_id: String, -) -> Result { - let success = state.grpc_client.delete_meeting(&meeting_id).await?; - - if success { - // Remove from cache - state.meetings.write().retain(|m| m.id != meeting_id); - - // Clear if selected - if state.selected_meeting.read().as_ref().map(|m| &m.id) == Some(&meeting_id) { - *state.selected_meeting.write() = None; - } - } - - Ok(success) -} - -/// Select a meeting for review (loads segments, annotations, audio) -#[tauri::command] -pub async fn select_meeting( - app: AppHandle, - state: State<'_, Arc>, - meeting_id: String, -) -> Result { - // Stop any current playback - { - let playback_guard = state.audio_playback.read(); - if let Some(ref handle) = *playback_guard { - let _ = handle.stop(); - } - } - *state.audio_playback.write() = None; - *state.playback_state.write() = crate::state::PlaybackState::Stopped; - *state.playback_position.write() = 0.0; - *state.playback_samples_played.write() = 0; - *state.highlighted_segment_index.write() = None; - let _ = app.emit(Events::PLAYBACK_STATE, "stopped"); - let _ = app.emit(Events::PLAYBACK_POSITION, 0.0); - let _ = app.emit(Events::HIGHLIGHT_CHANGE, serde_json::Value::Null); - - // Fetch meeting with segments - let details = state - .grpc_client - .get_meeting(&meeting_id, true, true) - .await - .map_err(|e| e.to_string())?; - - // Fetch annotations - let annotations = state - .grpc_client - .list_annotations(&meeting_id, 0.0, 0.0) - .await - .map_err(|e| e.to_string())?; - - // Load audio file if exists - let audio_result = load_meeting_audio(&state, &meeting_id); - - // Update state - *state.selected_meeting.write() = Some(details.meeting.clone()); - *state.current_meeting.write() = Some(details.meeting.clone()); - state - .transcript_segments - .write() - .clone_from(&details.segments); - *state.annotations.write() = annotations; - *state.current_summary.write() = details.summary.clone(); - *state.highlighted_segment_index.write() = None; - - // Apply audio state - if let Ok((buffer, duration, sample_rate)) = audio_result { - *state.session_audio_buffer.write() = buffer; - *state.playback_duration.write() = duration; - *state.playback_position.write() = 0.0; - *state.playback_sample_rate.write() = sample_rate; - } else { - // Clear audio buffer if no audio file - state.session_audio_buffer.write().clear(); - *state.playback_duration.write() = 0.0; - *state.playback_position.write() = 0.0; - *state.playback_sample_rate.write() = DEFAULT_SAMPLE_RATE; - } - - Ok(details) -} - -/// Load audio file for a meeting from the meetings directory. -fn load_meeting_audio( - state: &Arc, - meeting_id: &str, -) -> Result<(Vec, f64, u32), String> { - let meetings_dir = state.meetings_dir.read().clone(); - let audio_path = meetings_dir.join(format!("{}.nfaudio", meeting_id)); - - if !audio_path.exists() { - return Err("Audio file not found".to_string()); - } - - let (buffer, sample_rate) = load_audio_file(&state.crypto, &audio_path)?; - let duration: f64 = buffer.iter().map(|ta| ta.duration).sum(); - - Ok((buffer, duration, sample_rate)) -} diff --git a/client/src-tauri/src/commands/mod.rs b/client/src-tauri/src/commands/mod.rs deleted file mode 100644 index 364e251..0000000 --- a/client/src-tauri/src/commands/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Tauri command handlers -//! -//! This module contains all the command handlers that are exposed -//! to the React frontend via Tauri's invoke mechanism. - -pub mod annotation; -pub mod audio; -pub mod connection; -pub mod diarization; -pub mod export; -pub mod meeting; -pub mod playback; -pub mod preferences; -pub mod recording; -pub mod summary; -pub mod triggers; - -#[cfg(test)] -mod playback_tests; -#[cfg(test)] -mod recording_tests; diff --git a/client/src-tauri/src/commands/playback.rs b/client/src-tauri/src/commands/playback.rs deleted file mode 100644 index 4fb4880..0000000 --- a/client/src-tauri/src/commands/playback.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! Playback-related Tauri commands - -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; - -use tauri::{AppHandle, Emitter, State}; - -use crate::audio::PlaybackHandle; -use crate::constants::Events; -use crate::state::{AppState, PlaybackInfo, PlaybackState}; - -/// Position update interval in milliseconds. -const POSITION_UPDATE_INTERVAL_MS: u64 = 50; - -/// Start or resume playback. -#[tauri::command] -pub async fn play(app: AppHandle, state: State<'_, Arc>) -> Result<(), String> { - let current_state = *state.playback_state.read(); - - match current_state { - PlaybackState::Stopped => { - start_playback(&app, &state)?; - } - PlaybackState::Paused => { - resume_playback(&app, &state)?; - } - PlaybackState::Playing => { - // Already playing - } - } - - let _ = app.emit(Events::PLAYBACK_STATE, "playing"); - Ok(()) -} - -/// Pause playback. -#[tauri::command] -pub async fn pause(app: AppHandle, state: State<'_, Arc>) -> Result<(), String> { - if let Some(ref handle) = *state.audio_playback.read() { - handle.pause()?; - } - *state.playback_state.write() = PlaybackState::Paused; - let _ = app.emit(Events::PLAYBACK_STATE, "paused"); - Ok(()) -} - -/// Stop playback. -#[tauri::command] -pub async fn stop(app: AppHandle, state: State<'_, Arc>) -> Result<(), String> { - { - let playback_guard = state.audio_playback.read(); - if let Some(ref handle) = *playback_guard { - let _ = handle.stop(); - } - } - *state.audio_playback.write() = None; - *state.playback_state.write() = PlaybackState::Stopped; - *state.playback_position.write() = 0.0; - *state.playback_samples_played.write() = 0; - *state.highlighted_segment_index.write() = None; - - let _ = app.emit(Events::PLAYBACK_STATE, "stopped"); - let _ = app.emit(Events::PLAYBACK_POSITION, 0.0); - let _ = app.emit(Events::HIGHLIGHT_CHANGE, serde_json::Value::Null); - Ok(()) -} - -/// Seek to position. -#[tauri::command] -pub async fn seek( - app: AppHandle, - state: State<'_, Arc>, - position: f64, -) -> Result { - if !position.is_finite() { - return Err("Invalid seek position".to_string()); - } - - let duration = *state.playback_duration.read(); - let clamped = position.clamp(0.0, duration.max(0.0)); - - *state.playback_position.write() = clamped; - - // Update highlight - let highlighted_segment = state.find_segment_at_position(clamped); - *state.highlighted_segment_index.write() = highlighted_segment; - - if let Some(index) = highlighted_segment { - let _ = app.emit(Events::HIGHLIGHT_CHANGE, index); - } else { - let _ = app.emit(Events::HIGHLIGHT_CHANGE, serde_json::Value::Null); - } - let _ = app.emit(Events::PLAYBACK_POSITION, clamped); - - let playback_state = *state.playback_state.read(); - - Ok(PlaybackInfo { - state: playback_state, - position: clamped, - duration, - highlighted_segment, - }) -} - -/// Get current playback state. -#[tauri::command] -pub async fn get_playback_state(state: State<'_, Arc>) -> Result { - Ok(PlaybackInfo { - state: *state.playback_state.read(), - position: *state.playback_position.read(), - duration: *state.playback_duration.read(), - highlighted_segment: *state.highlighted_segment_index.read(), - }) -} - -// ============================================================================ -// Internal helpers -// ============================================================================ - -fn start_playback(app: &AppHandle, state: &Arc) -> Result<(), String> { - let audio_buffer = state.session_audio_buffer.read().clone(); - if audio_buffer.is_empty() { - return Err("No audio to play".to_string()); - } - - let sample_rate = *state.playback_sample_rate.read(); - if sample_rate == 0 { - return Err("Invalid sample rate for playback".to_string()); - } - - let handle = PlaybackHandle::new()?; - let started = handle.play(audio_buffer, sample_rate)?; - - *state.playback_duration.write() = started.duration; - *state.playback_position.write() = 0.0; - *state.playback_samples_played.write() = 0; - *state.playback_state.write() = PlaybackState::Playing; - - // Store handle - *state.audio_playback.write() = Some(handle); - - // Spawn position tracking thread - spawn_position_tracker( - app.clone(), - Arc::clone(state), - started.playing_flag, - started.position_atomic, - sample_rate, - 0, // Start from beginning - ); - - Ok(()) -} - -fn resume_playback(app: &AppHandle, state: &Arc) -> Result<(), String> { - let playback_guard = state.audio_playback.read(); - let handle = playback_guard - .as_ref() - .ok_or("No playback to resume")?; - - handle.resume()?; - drop(playback_guard); - - *state.playback_state.write() = PlaybackState::Playing; - - // Respawn position tracker with accumulated samples - let samples_played = *state.playback_samples_played.read(); - let sample_rate = *state.playback_sample_rate.read(); - - // We need to get the atomics again - create new ones for tracking - let playing_flag = Arc::new(std::sync::atomic::AtomicBool::new(true)); - let position_atomic = Arc::new(std::sync::atomic::AtomicU64::new( - (*state.playback_position.read()).to_bits(), - )); - - spawn_position_tracker( - app.clone(), - Arc::clone(state), - playing_flag, - position_atomic, - sample_rate, - samples_played, - ); - Ok(()) -} - -fn spawn_position_tracker( - app: AppHandle, - state: Arc, - playing_flag: Arc, - position_atomic: Arc, - sample_rate: u32, - initial_samples: u64, -) { - let sample_rate = (sample_rate.max(1)) as f64; - - std::thread::spawn(move || { - let mut samples_played: u64 = initial_samples; - let mut last_highlight: Option = None; - - loop { - // Check playback state from AppState - let current_state = *state.playback_state.read(); - - if current_state == PlaybackState::Stopped { - break; - } - - if current_state == PlaybackState::Paused { - // Save accumulated samples for resume - *state.playback_samples_played.write() = samples_played; - playing_flag.store(false, Ordering::SeqCst); - // Exit thread when paused - will be respawned on resume - break; - } - - // Calculate position from samples played - samples_played += (sample_rate * POSITION_UPDATE_INTERVAL_MS as f64 / 1000.0) as u64; - let position = samples_played as f64 / sample_rate; - - // Update position atomic and state - position_atomic.store(position.to_bits(), Ordering::SeqCst); - *state.playback_position.write() = position; - - // Check if finished - let duration = *state.playback_duration.read(); - if position >= duration { - *state.playback_state.write() = PlaybackState::Stopped; - *state.playback_position.write() = duration; - *state.highlighted_segment_index.write() = None; - playing_flag.store(false, Ordering::SeqCst); - let _ = app.emit(Events::PLAYBACK_STATE, "stopped"); - let _ = app.emit(Events::PLAYBACK_POSITION, duration); - let _ = app.emit(Events::HIGHLIGHT_CHANGE, serde_json::Value::Null); - break; - } - - // Update highlighted segment - let highlight = state.find_segment_at_position(position); - if highlight != last_highlight { - last_highlight = highlight; - *state.highlighted_segment_index.write() = highlight; - if let Some(index) = highlight { - let _ = app.emit(Events::HIGHLIGHT_CHANGE, index); - } else { - let _ = app.emit(Events::HIGHLIGHT_CHANGE, serde_json::Value::Null); - } - } - - // Emit position update - let _ = app.emit(Events::PLAYBACK_POSITION, position); - - std::thread::sleep(Duration::from_millis(POSITION_UPDATE_INTERVAL_MS)); - } - }); -} diff --git a/client/src-tauri/src/commands/playback_tests.rs b/client/src-tauri/src/commands/playback_tests.rs deleted file mode 100644 index d1a0e60..0000000 --- a/client/src-tauri/src/commands/playback_tests.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Unit tests for playback commands -//! -//! These tests verify the playback state machine and seek behavior. - -#[cfg(test)] -mod tests { - use crate::state::{PlaybackInfo, PlaybackState}; - - #[test] - fn playback_state_default() { - let state = PlaybackState::default(); - assert_eq!(state, PlaybackState::Stopped); - } - - #[test] - fn playback_state_serialization() { - assert_eq!( - serde_json::to_string(&PlaybackState::Stopped).unwrap(), - "\"stopped\"" - ); - assert_eq!( - serde_json::to_string(&PlaybackState::Playing).unwrap(), - "\"playing\"" - ); - assert_eq!( - serde_json::to_string(&PlaybackState::Paused).unwrap(), - "\"paused\"" - ); - } - - #[test] - fn playback_info_construction() { - let info = PlaybackInfo { - state: PlaybackState::Playing, - position: 30.5, - duration: 120.0, - highlighted_segment: Some(5), - }; - - assert_eq!(info.state, PlaybackState::Playing); - assert!((info.position - 30.5).abs() < f64::EPSILON); - assert!((info.duration - 120.0).abs() < f64::EPSILON); - assert_eq!(info.highlighted_segment, Some(5)); - } - - #[test] - fn playback_info_without_highlight() { - let info = PlaybackInfo { - state: PlaybackState::Stopped, - position: 0.0, - duration: 0.0, - highlighted_segment: None, - }; - - assert_eq!(info.highlighted_segment, None); - } - - #[test] - fn seek_position_clamping() { - // Test the clamping logic that would be used in the seek command - let duration = 100.0_f64; - - // Normal position - let pos = 50.0_f64; - let clamped = pos.clamp(0.0, duration.max(0.0)); - assert!((clamped - 50.0).abs() < f64::EPSILON); - - // Position beyond duration - let pos = 150.0_f64; - let clamped = pos.clamp(0.0, duration.max(0.0)); - assert!((clamped - 100.0).abs() < f64::EPSILON); - - // Negative position - let pos = -10.0_f64; - let clamped = pos.clamp(0.0, duration.max(0.0)); - assert!((clamped - 0.0).abs() < f64::EPSILON); - } - - #[test] - fn seek_position_with_zero_duration() { - let duration = 0.0_f64; - let pos = 50.0_f64; - let clamped = pos.clamp(0.0, duration.max(0.0)); - assert!((clamped - 0.0).abs() < f64::EPSILON); - } - - #[test] - fn validate_finite_position() { - assert!(10.0_f64.is_finite()); - assert!(!f64::INFINITY.is_finite()); - assert!(!f64::NEG_INFINITY.is_finite()); - assert!(!f64::NAN.is_finite()); - } -} diff --git a/client/src-tauri/src/commands/preferences.rs b/client/src-tauri/src/commands/preferences.rs deleted file mode 100644 index 13112a1..0000000 --- a/client/src-tauri/src/commands/preferences.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! User preferences commands -//! -//! Handles getting and saving user preferences to persistent storage. - -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; - -use keyring::Entry; -use tauri::State; - -use crate::constants::{preferences as prefs_config, secrets}; -use crate::state::AppState; - -use serde::{Deserialize, Serialize}; - -/// Summarization provider options -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum SummarizationProvider { - #[default] - None, - Cloud, - Ollama, -} - -/// User preferences structure -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserPreferences { - pub server_url: String, - pub data_directory: String, - pub encryption_enabled: bool, - pub auto_start_enabled: bool, - pub trigger_confidence_threshold: f32, - pub summarization_provider: SummarizationProvider, - pub cloud_api_key: String, - pub ollama_url: String, -} - -impl Default for UserPreferences { - fn default() -> Self { - Self { - server_url: "localhost:50051".to_string(), - data_directory: get_default_data_dir(), - encryption_enabled: true, - auto_start_enabled: true, - trigger_confidence_threshold: 0.7, - summarization_provider: SummarizationProvider::None, - cloud_api_key: String::new(), - ollama_url: "http://localhost:11434".to_string(), - } - } -} - -/// Get default data directory path -fn get_default_data_dir() -> String { - directories::ProjectDirs::from("com", "noteflow", "NoteFlow") - .map(|d| d.data_dir().to_string_lossy().to_string()) - .unwrap_or_else(|| { - directories::BaseDirs::new() - .map(|d| d.home_dir().join(".noteflow").to_string_lossy().to_string()) - .unwrap_or_else(|| "/tmp/noteflow".to_string()) - }) -} - -// ============================================================================ -// Preferences Persistence -// ============================================================================ - -/// Get the preferences file path -fn get_preferences_path() -> PathBuf { - directories::ProjectDirs::from("com", "noteflow", "NoteFlow") - .map(|d| d.config_dir().join(prefs_config::FILENAME)) - .unwrap_or_else(|| PathBuf::from("/tmp/noteflow").join(prefs_config::FILENAME)) -} - -/// Load preferences from disk -pub fn load_preferences_from_disk() -> HashMap { - let path = get_preferences_path(); - if path.exists() { - std::fs::read_to_string(&path) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default() - } else { - HashMap::new() - } -} - -/// Save preferences to disk -fn persist_preferences(prefs: &HashMap) -> Result<(), String> { - let path = get_preferences_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create config directory: {e}"))?; - } - let json = serde_json::to_string_pretty(prefs) - .map_err(|e| format!("Failed to serialize preferences: {e}"))?; - std::fs::write(&path, json).map_err(|e| format!("Failed to save preferences: {e}")) -} - -// ============================================================================ -// Secure API Key Storage -// ============================================================================ - -/// Get API key from system keychain -fn get_api_key() -> Option { - Entry::new(secrets::KEYCHAIN_SERVICE, secrets::API_KEY_USERNAME) - .ok() - .and_then(|entry| entry.get_password().ok()) - .filter(|s| !s.is_empty()) -} - -/// Store API key in system keychain -fn set_api_key(key: &str) -> Result<(), String> { - let entry = Entry::new(secrets::KEYCHAIN_SERVICE, secrets::API_KEY_USERNAME) - .map_err(|e| format!("Keychain access failed: {e}"))?; - - if key.is_empty() { - // Delete credential if key is empty - entry.delete_password().ok(); - } else { - entry - .set_password(key) - .map_err(|e| format!("Failed to store API key: {e}"))?; - } - Ok(()) -} - -/// Mask API key for display (show first/last 4 chars) -fn mask_api_key(key: &str) -> String { - if key.is_empty() { - String::new() - } else if key.len() <= 8 { - "*".repeat(key.len()) - } else { - format!("{}...{}", &key[..4], &key[key.len() - 4..]) - } -} - -/// Get user preferences -#[tauri::command] -pub async fn get_preferences(state: State<'_, Arc>) -> Result { - let prefs = state.preferences.read(); - - // Get API key from keychain and mask it for display - let masked_api_key = get_api_key().map(|k| mask_api_key(&k)).unwrap_or_default(); - - // Reconstruct preferences from stored values - Ok(UserPreferences { - server_url: prefs - .get("server_url") - .and_then(|v| v.as_str()) - .unwrap_or("localhost:50051") - .to_string(), - data_directory: prefs - .get("data_directory") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(get_default_data_dir), - encryption_enabled: prefs - .get("encryption_enabled") - .and_then(|v| v.as_bool()) - .unwrap_or(true), - auto_start_enabled: prefs - .get("auto_start_enabled") - .and_then(|v| v.as_bool()) - .unwrap_or(true), - trigger_confidence_threshold: prefs - .get("trigger_confidence_threshold") - .and_then(|v| v.as_f64()) - .map(|v| v as f32) - .unwrap_or(0.7), - summarization_provider: prefs - .get("summarization_provider") - .and_then(|v| v.as_str()) - .map(|s| match s { - "cloud" => SummarizationProvider::Cloud, - "ollama" => SummarizationProvider::Ollama, - _ => SummarizationProvider::None, - }) - .unwrap_or_default(), - cloud_api_key: masked_api_key, - ollama_url: prefs - .get("ollama_url") - .and_then(|v| v.as_str()) - .unwrap_or("http://localhost:11434") - .to_string(), - }) -} - -/// Save user preferences -#[tauri::command] -pub async fn save_preferences( - state: State<'_, Arc>, - preferences: UserPreferences, -) -> Result<(), String> { - // Store API key in keychain if user entered a new one (not masked) - let api_key = &preferences.cloud_api_key; - if !api_key.contains("...") && !api_key.contains("*") { - set_api_key(api_key)?; - } - - let mut prefs = state.preferences.write(); - - prefs.insert( - "server_url".to_string(), - serde_json::json!(preferences.server_url), - ); - prefs.insert( - "data_directory".to_string(), - serde_json::json!(preferences.data_directory), - ); - prefs.insert( - "encryption_enabled".to_string(), - serde_json::json!(preferences.encryption_enabled), - ); - prefs.insert( - "auto_start_enabled".to_string(), - serde_json::json!(preferences.auto_start_enabled), - ); - prefs.insert( - "trigger_confidence_threshold".to_string(), - serde_json::json!(preferences.trigger_confidence_threshold), - ); - prefs.insert( - "summarization_provider".to_string(), - serde_json::json!(match preferences.summarization_provider { - SummarizationProvider::Cloud => "cloud", - SummarizationProvider::Ollama => "ollama", - SummarizationProvider::None => "none", - }), - ); - prefs.insert( - "ollama_url".to_string(), - serde_json::json!(preferences.ollama_url), - ); - // Note: cloud_api_key is stored in keychain, not in preferences file - - // Update meetings directory if changed - let meetings_dir = std::path::PathBuf::from(&preferences.data_directory).join("meetings"); - *state.meetings_dir.write() = meetings_dir; - - // Persist to disk - persist_preferences(&prefs)?; - - Ok(()) -} diff --git a/client/src-tauri/src/commands/recording.rs b/client/src-tauri/src/commands/recording.rs deleted file mode 100644 index 03dd629..0000000 --- a/client/src-tauri/src/commands/recording.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Recording-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::grpc::types::MeetingInfo; -use crate::helpers::now_timestamp; -use crate::state::AppState; - -/// Start recording a new meeting -#[tauri::command] -pub async fn start_recording( - state: State<'_, Arc>, - title: String, -) -> Result { - // Check connection (from gRPC client - single source of truth) - if !state.grpc_client.is_connected() { - return Err("Not connected to server".to_string()); - } - - // Check not already recording (local state) - if *state.recording.read() { - return Err("Already recording".to_string()); - } - - // Create meeting via gRPC - let meeting = state - .grpc_client - .create_meeting(&title, None) - .await - .map_err(|e| e.to_string())?; - - // Start gRPC streaming - if this fails, clean up the meeting reference - if let Err(e) = state.grpc_client.start_streaming(&meeting.id).await { - // Cleanup: don't leave orphaned meeting in state - *state.current_meeting.write() = None; - - // Best-effort cleanup to avoid orphaned meetings on the server. - let _ = state.grpc_client.stop_meeting(&meeting.id).await; - let _ = state.grpc_client.delete_meeting(&meeting.id).await; - - return Err(e.to_string()); - } - - // All operations succeeded - update state atomically - *state.current_meeting.write() = Some(meeting.clone()); - *state.recording.write() = true; - *state.recording_start_time.write() = Some(now_timestamp()); - - // Clear previous session - state.clear_transcript(); - state.clear_session_audio(); - - Ok(meeting) -} - -/// Stop recording -#[tauri::command] -pub async fn stop_recording(state: State<'_, Arc>) -> Result { - // Guard against multiple stops - if !*state.recording.read() { - return Err("Not recording".to_string()); - } - - // Stop gRPC streaming first (best-effort) - state.grpc_client.stop_streaming().await; - - // Get current meeting - reset recording state if missing to prevent stuck UI - let meeting = state.current_meeting.read().clone().ok_or_else(|| { - // Avoid leaving the app stuck "recording" without a meeting reference - *state.recording.write() = false; - *state.recording_start_time.write() = None; - *state.elapsed_seconds.write() = 0; - "No current meeting".to_string() - })?; - - // Stop on server; if it fails, still reset local recording state to prevent stuck UI - let updated_meeting = match state.grpc_client.stop_meeting(&meeting.id).await { - Ok(m) => m, - Err(e) => { - *state.recording.write() = false; - *state.recording_start_time.write() = None; - *state.elapsed_seconds.write() = 0; - return Err(e.to_string()); - } - }; - - // Reset recording state and update meeting reference for playback - *state.recording.write() = false; - *state.recording_start_time.write() = None; - *state.elapsed_seconds.write() = 0; - *state.current_meeting.write() = Some(updated_meeting.clone()); - - Ok(updated_meeting) -} diff --git a/client/src-tauri/src/commands/recording_tests.rs b/client/src-tauri/src/commands/recording_tests.rs deleted file mode 100644 index 798a5a4..0000000 --- a/client/src-tauri/src/commands/recording_tests.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Unit tests for recording commands -//! -//! These tests verify recording state management and error handling. - -#[cfg(test)] -mod tests { - use crate::helpers::now_timestamp; - - #[test] - fn now_timestamp_is_positive() { - let ts = now_timestamp(); - assert!(ts > 0.0, "Timestamp should be positive"); - } - - #[test] - fn now_timestamp_is_increasing() { - let ts1 = now_timestamp(); - std::thread::sleep(std::time::Duration::from_millis(10)); - let ts2 = now_timestamp(); - assert!(ts2 >= ts1, "Second timestamp should be >= first"); - } - - #[test] - fn recording_state_transitions() { - // Test valid state transitions - // false -> true (start recording) - // true -> false (stop recording) - let mut recording = false; - - // Start recording - recording = true; - assert!(recording); - - // Stop recording - recording = false; - assert!(!recording); - } - - #[test] - fn elapsed_seconds_calculation() { - let start = now_timestamp(); - std::thread::sleep(std::time::Duration::from_millis(100)); - let end = now_timestamp(); - let elapsed = (end - start) as u32; - // Should be 0 since we slept < 1 second - assert_eq!(elapsed, 0); - } - - #[test] - fn elapsed_seconds_formatting() { - // Test formatting of elapsed time - let seconds = 3661_u32; // 1 hour, 1 minute, 1 second - - let hours = seconds / 3600; - let minutes = (seconds % 3600) / 60; - let secs = seconds % 60; - - assert_eq!(hours, 1); - assert_eq!(minutes, 1); - assert_eq!(secs, 1); - } -} diff --git a/client/src-tauri/src/commands/summary.rs b/client/src-tauri/src/commands/summary.rs deleted file mode 100644 index c5b2fc7..0000000 --- a/client/src-tauri/src/commands/summary.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Summary-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::grpc::types::SummaryInfo; -use crate::state::AppState; - -/// Generate AI summary for a meeting -#[tauri::command] -pub async fn generate_summary( - state: State<'_, Arc>, - meeting_id: String, - force_regenerate: bool, -) -> Result { - // Set loading state - *state.summary_loading.write() = true; - *state.summary_error.write() = None; - - let result = state - .grpc_client - .generate_summary(&meeting_id, force_regenerate) - .await; - - // Update state - *state.summary_loading.write() = false; - - match result { - Ok(summary) => { - *state.current_summary.write() = Some(summary.clone()); - Ok(summary) - } - Err(e) => { - let error_msg = e.to_string(); - *state.summary_error.write() = Some(error_msg.clone()); - Err(error_msg) - } - } -} diff --git a/client/src-tauri/src/commands/triggers.rs b/client/src-tauri/src/commands/triggers.rs deleted file mode 100644 index 53f7752..0000000 --- a/client/src-tauri/src/commands/triggers.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Trigger-related Tauri commands - -use std::sync::Arc; -use tauri::State; - -use crate::grpc::types::MeetingInfo; -use crate::helpers::now_timestamp; -use crate::state::{AppState, TriggerStatus}; - -/// Enable or disable trigger detection -#[tauri::command] -pub async fn set_trigger_enabled( - state: State<'_, Arc>, - enabled: bool, -) -> Result<(), String> { - *state.trigger_enabled.write() = enabled; - Ok(()) -} - -/// Snooze triggers for a duration -#[tauri::command] -pub async fn snooze_triggers( - state: State<'_, Arc>, - minutes: Option, -) -> Result<(), String> { - if let Some(ref mut service) = *state.trigger_service.lock().await { - service.snooze(minutes.map(|m| m as f64 * 60.0)); - } - *state.trigger_pending.write() = false; - *state.trigger_decision.write() = None; - Ok(()) -} - -/// Reset snooze -#[tauri::command] -pub async fn reset_snooze(state: State<'_, Arc>) -> Result<(), String> { - if let Some(ref mut service) = *state.trigger_service.lock().await { - service.reset_snooze(); - } - Ok(()) -} - -/// Get trigger status -#[tauri::command] -pub async fn get_trigger_status(state: State<'_, Arc>) -> Result { - let service = state.trigger_service.lock().await; - - Ok(TriggerStatus { - enabled: *state.trigger_enabled.read(), - pending: *state.trigger_pending.read(), - decision: state.trigger_decision.read().clone(), - snoozed: service.as_ref().map(|s| s.is_snoozed()).unwrap_or(false), - snooze_remaining: service - .as_ref() - .map(|s| s.snooze_remaining_seconds()) - .unwrap_or(0.0), - }) -} - -/// Dismiss pending trigger -#[tauri::command] -pub async fn dismiss_trigger(state: State<'_, Arc>) -> Result<(), String> { - *state.trigger_pending.write() = false; - *state.trigger_decision.write() = None; - Ok(()) -} - -/// Accept trigger and start recording -#[tauri::command] -pub async fn accept_trigger( - state: State<'_, Arc>, - title: Option, -) -> Result { - // Same safety checks as manual recording start - if !state.grpc_client.is_connected() { - return Err("Not connected to server".to_string()); - } - if *state.recording.read() { - return Err("Already recording".to_string()); - } - - // Start recording with trigger-detected title - let title = title.unwrap_or_else(|| { - state - .trigger_decision - .read() - .as_ref() - .and_then(|d| d.detected_app.clone()) - .map(|app| format!("{} Meeting", app)) - .unwrap_or_else(|| "Auto-detected Meeting".to_string()) - }); - - // Create meeting via gRPC - let meeting = state.grpc_client.create_meeting(&title, None).await?; - - // Start streaming; don't update local state unless this succeeds - if let Err(e) = state.grpc_client.start_streaming(&meeting.id).await { - *state.current_meeting.write() = None; - - // Best-effort cleanup to avoid orphaned meetings on the server. - let _ = state.grpc_client.stop_meeting(&meeting.id).await; - let _ = state.grpc_client.delete_meeting(&meeting.id).await; - - // Keep decision/pending so the user can retry or dismiss. - *state.trigger_pending.write() = true; - return Err(e.to_string()); - } - - // Clear pending UI state only after successful start to avoid losing the prompt on transient errors - *state.trigger_pending.write() = false; - - // All operations succeeded - update state - *state.current_meeting.write() = Some(meeting.clone()); - *state.recording.write() = true; - *state.recording_start_time.write() = Some(now_timestamp()); - *state.trigger_decision.write() = None; - state.clear_transcript(); - state.clear_session_audio(); - - Ok(meeting) -} diff --git a/client/src-tauri/src/config.rs b/client/src-tauri/src/config.rs deleted file mode 100644 index 97d9c2b..0000000 --- a/client/src-tauri/src/config.rs +++ /dev/null @@ -1,286 +0,0 @@ -//! Application configuration -//! -//! Centralized configuration loaded from environment variables with sensible defaults. -//! All configurable values should be defined here rather than hardcoded. - -use std::env; -use std::path::PathBuf; -use std::sync::OnceLock; -use std::time::Duration; - -/// Global configuration instance -static CONFIG: OnceLock = OnceLock::new(); - -/// Get the global configuration (initializes on first access) -pub fn config() -> &'static AppConfig { - CONFIG.get_or_init(AppConfig::from_env) -} - -/// Application configuration -#[derive(Debug, Clone)] -pub struct AppConfig { - /// Server configuration - pub server: ServerConfig, - /// Audio configuration - pub audio: AudioConfig, - /// Storage configuration - pub storage: StorageConfig, - /// Trigger configuration - pub triggers: TriggerConfig, - /// Cache configuration - pub cache: CacheConfig, -} - -impl AppConfig { - /// Load configuration from environment variables - pub fn from_env() -> Self { - Self { - server: ServerConfig::from_env(), - audio: AudioConfig::from_env(), - storage: StorageConfig::from_env(), - triggers: TriggerConfig::from_env(), - cache: CacheConfig::from_env(), - } - } -} - -/// Server connection configuration -#[derive(Debug, Clone)] -pub struct ServerConfig { - /// Default server address - pub default_address: String, - /// Connection timeout - pub connect_timeout: Duration, - /// Request timeout - pub request_timeout: Duration, - /// Maximum retry attempts - pub max_retries: u32, - /// Retry backoff base (milliseconds) - pub retry_backoff_ms: u64, -} - -impl ServerConfig { - fn from_env() -> Self { - Self { - default_address: env::var("NOTEFLOW_SERVER_ADDRESS") - .unwrap_or_else(|_| "localhost:50051".to_string()), - connect_timeout: Duration::from_secs( - env::var("NOTEFLOW_CONNECT_TIMEOUT_SECS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(5), - ), - request_timeout: Duration::from_secs( - env::var("NOTEFLOW_REQUEST_TIMEOUT_SECS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(30), - ), - max_retries: env::var("NOTEFLOW_MAX_RETRIES") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(3), - retry_backoff_ms: env::var("NOTEFLOW_RETRY_BACKOFF_MS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1000), - } - } -} - -/// Audio capture/playback configuration -#[derive(Debug, Clone)] -pub struct AudioConfig { - /// Default sample rate (Hz) - pub sample_rate: u32, - /// Default number of channels - pub channels: u32, - /// Buffer size in frames - pub buffer_size: u32, - /// Minimum dB level (silence) - pub min_db_level: f32, - /// Maximum dB level - pub max_db_level: f32, - /// VU meter update rate (Hz) - pub vu_update_rate: u32, -} - -impl AudioConfig { - fn from_env() -> Self { - Self { - sample_rate: env::var("NOTEFLOW_SAMPLE_RATE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(16000), - channels: env::var("NOTEFLOW_AUDIO_CHANNELS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1), - buffer_size: env::var("NOTEFLOW_AUDIO_BUFFER_SIZE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1024), - min_db_level: env::var("NOTEFLOW_MIN_DB_LEVEL") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(-60.0), - max_db_level: env::var("NOTEFLOW_MAX_DB_LEVEL") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0.0), - vu_update_rate: env::var("NOTEFLOW_VU_UPDATE_RATE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(20), - } - } -} - -/// Storage configuration -#[derive(Debug, Clone)] -pub struct StorageConfig { - /// Base directory for meetings - pub meetings_dir: PathBuf, - /// Enable audio encryption - pub encrypt_audio: bool, - /// Maximum audio file size (bytes) - pub max_audio_size: u64, -} - -impl StorageConfig { - fn from_env() -> Self { - let default_meetings_dir = directories::BaseDirs::new() - .map(|dirs| dirs.home_dir().join(".noteflow").join("meetings")) - .unwrap_or_else(|| PathBuf::from("/tmp/noteflow/meetings")); - - Self { - meetings_dir: env::var("NOTEFLOW_MEETINGS_DIR") - .map(PathBuf::from) - .unwrap_or(default_meetings_dir), - encrypt_audio: env::var("NOTEFLOW_ENCRYPT_AUDIO") - .map(|v| v.to_lowercase() != "false" && v != "0") - .unwrap_or(true), - max_audio_size: env::var("NOTEFLOW_MAX_AUDIO_SIZE") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(500 * 1024 * 1024), // 500 MB default - } - } -} - -/// Trigger detection configuration -#[derive(Debug, Clone)] -pub struct TriggerConfig { - /// Enable trigger detection - pub enabled: bool, - /// Polling interval - pub poll_interval: Duration, - /// Default snooze duration - pub snooze_duration: Duration, - /// Auto-start threshold (confidence 0.0-1.0) - pub auto_start_threshold: f32, -} - -impl TriggerConfig { - fn from_env() -> Self { - let auto_start_threshold: f32 = env::var("NOTEFLOW_AUTO_START_THRESHOLD") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(0.8_f32) - .clamp(0.0, 1.0); // Ensure threshold is within valid range - - Self { - enabled: env::var("NOTEFLOW_TRIGGERS_ENABLED") - .map(|v| v.to_lowercase() == "true" || v == "1") - .unwrap_or(false), - poll_interval: Duration::from_secs( - env::var("NOTEFLOW_TRIGGER_POLL_SECS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(5) - .max(1), // Minimum 1 second poll interval - ), - snooze_duration: Duration::from_secs( - env::var("NOTEFLOW_SNOOZE_DURATION_SECS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(300) - .max(1), // Minimum 1 second snooze - ), - auto_start_threshold, - } - } -} - -/// Cache configuration -#[derive(Debug, Clone)] -pub struct CacheConfig { - /// Cache backend type - pub backend: CacheBackend, - /// Redis URL (if using Redis) - pub redis_url: Option, - /// Default TTL for cached items (seconds) - pub default_ttl_secs: u64, - /// Maximum memory cache size (items) - pub max_memory_items: usize, -} - -/// Supported cache backends -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CacheBackend { - /// In-memory cache (default) - Memory, - /// Redis cache - Redis, - /// No caching - None, -} - -impl CacheConfig { - fn from_env() -> Self { - let backend = match env::var("NOTEFLOW_CACHE_BACKEND") - .unwrap_or_default() - .to_lowercase() - .as_str() - { - "redis" => CacheBackend::Redis, - "none" | "disabled" => CacheBackend::None, - _ => CacheBackend::Memory, - }; - - Self { - backend, - redis_url: env::var("NOTEFLOW_REDIS_URL").ok(), - default_ttl_secs: env::var("NOTEFLOW_CACHE_TTL_SECS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(300), - max_memory_items: env::var("NOTEFLOW_CACHE_MAX_ITEMS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(1000), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_config_is_valid() { - let config = AppConfig::from_env(); - - assert!(!config.server.default_address.is_empty()); - assert!(config.server.connect_timeout.as_secs() > 0); - assert!(config.audio.sample_rate > 0); - assert!(config.triggers.auto_start_threshold >= 0.0); - assert!(config.triggers.auto_start_threshold <= 1.0); - } - - #[test] - fn cache_backend_parsing() { - // Default is Memory - assert_eq!(CacheConfig::from_env().backend, CacheBackend::Memory); - } -} diff --git a/client/src-tauri/src/constants.rs b/client/src-tauri/src/constants.rs deleted file mode 100644 index 2e44c6a..0000000 --- a/client/src-tauri/src/constants.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Application-wide constants -//! -//! Centralized location for all hardcoded values to ensure consistency -//! between Rust backend and TypeScript frontend. - -/// Event names emitted from backend to frontend -pub mod events { - /// Transcript update (partial or final segment) - pub const TRANSCRIPT_UPDATE: &str = "TRANSCRIPT_UPDATE"; - /// Audio level change (normalized 0.0-1.0) - pub const AUDIO_LEVEL: &str = "AUDIO_LEVEL"; - /// Playback position change (seconds) - pub const PLAYBACK_POSITION: &str = "PLAYBACK_POSITION"; - /// Playback state change (playing/paused/stopped) - pub const PLAYBACK_STATE: &str = "PLAYBACK_STATE"; - /// Highlighted segment index change - pub const HIGHLIGHT_CHANGE: &str = "HIGHLIGHT_CHANGE"; - /// Connection state change - pub const CONNECTION_CHANGE: &str = "CONNECTION_CHANGE"; - /// Meeting detected by trigger system - pub const MEETING_DETECTED: &str = "MEETING_DETECTED"; - /// Recording timer tick (elapsed seconds) - pub const RECORDING_TIMER: &str = "RECORDING_TIMER"; - /// Error event - pub const ERROR: &str = "ERROR"; - /// Summary generation progress - pub const SUMMARY_PROGRESS: &str = "SUMMARY_PROGRESS"; - /// Diarization job progress - pub const DIARIZATION_PROGRESS: &str = "DIARIZATION_PROGRESS"; -} - -// Backwards-compatible alias for existing call sites. -pub use events as Events; - -/// gRPC connection settings -pub mod grpc { - use std::time::Duration; - - /// Connection timeout - pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); - /// Request timeout - pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); - /// Default server port - pub const DEFAULT_PORT: u16 = 50051; -} - -/// Audio settings -pub mod audio { - /// Default sample rate (Hz) - pub const DEFAULT_SAMPLE_RATE: u32 = 16000; - /// Default number of channels - pub const DEFAULT_CHANNELS: u32 = 1; - /// Minimum dB level (silence) - pub const MIN_DB_LEVEL: f32 = -60.0; - /// Maximum dB level - pub const MAX_DB_LEVEL: f32 = 0.0; -} - -/// Crypto settings -pub mod crypto { - /// AES-GCM nonce size in bytes - pub const NONCE_SIZE: usize = 12; - /// AES-256 key size in bytes - pub const KEY_SIZE: usize = 32; - /// Keychain service name - pub const KEYCHAIN_SERVICE: &str = "noteflow"; - /// Keychain username for audio key - pub const KEYCHAIN_USERNAME: &str = "audio-key"; -} - -/// Trigger settings -pub mod triggers { - use std::time::Duration; - - /// Default snooze duration - pub const DEFAULT_SNOOZE_DURATION: Duration = Duration::from_secs(300); - /// Trigger polling interval - pub const POLL_INTERVAL: Duration = Duration::from_secs(5); -} - -/// Preferences settings -pub mod preferences { - /// Preferences filename - pub const FILENAME: &str = "preferences.json"; -} - -/// Secrets keychain settings -pub mod secrets { - /// Keychain service name for secrets - pub const KEYCHAIN_SERVICE: &str = "com.noteflow.secrets"; - /// Keychain username for cloud API key - pub const API_KEY_USERNAME: &str = "cloud_api_key"; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn event_names_are_uppercase() { - // Event names should be SCREAMING_SNAKE_CASE for convention - assert!(events::TRANSCRIPT_UPDATE - .chars() - .all(|c| c.is_uppercase() || c == '_')); - assert!(events::AUDIO_LEVEL - .chars() - .all(|c| c.is_uppercase() || c == '_')); - assert!(events::CONNECTION_CHANGE - .chars() - .all(|c| c.is_uppercase() || c == '_')); - } - - #[test] - fn grpc_timeouts_are_reasonable() { - assert!(grpc::CONNECTION_TIMEOUT.as_secs() >= 1); - assert!(grpc::CONNECTION_TIMEOUT.as_secs() <= 30); - assert!(grpc::REQUEST_TIMEOUT.as_secs() >= 5); - } - - #[test] - fn crypto_sizes_are_correct() { - // AES-256 requires 32-byte key - assert_eq!(crypto::KEY_SIZE, 32); - // GCM standard nonce is 12 bytes - assert_eq!(crypto::NONCE_SIZE, 12); - } -} diff --git a/client/src-tauri/src/crypto/mod.rs b/client/src-tauri/src/crypto/mod.rs deleted file mode 100644 index a89c3bf..0000000 --- a/client/src-tauri/src/crypto/mod.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Cryptographic functionality for audio encryption -//! -//! This module provides AES-256-GCM encryption for audio files -//! and keychain integration for secure key storage. - -use aes_gcm::{ - aead::{Aead, KeyInit}, - Aes256Gcm, Nonce, -}; -use rand::Rng; - -use crate::constants::crypto as crypto_config; - -/// Crypto box for encryption/decryption operations -pub struct CryptoBox { - cipher: Aes256Gcm, -} - -impl std::fmt::Debug for CryptoBox { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CryptoBox") - .field("cipher", &"[Aes256Gcm]") - .finish() - } -} - -impl CryptoBox { - /// Create a new CryptoBox with a key from keychain or generate new - pub fn new() -> Result { - let key = Self::get_or_create_key()?; - let cipher = Aes256Gcm::new_from_slice(&key) - .map_err(|e| format!("Failed to create cipher: {}", e))?; - - Ok(Self { cipher }) - } - - /// Get existing key from keychain or generate a new one - fn get_or_create_key() -> Result, String> { - // Try to get key from keychain - let keyring = keyring::Entry::new( - crypto_config::KEYCHAIN_SERVICE, - crypto_config::KEYCHAIN_USERNAME, - ) - .map_err(|e| format!("Failed to access keychain: {}", e))?; - - match keyring.get_password() { - Ok(key_hex) => { - // Decode existing key; if corrupted, regenerate to avoid bricking startup. - match hex_decode(&key_hex) { - Ok(key) if key.len() == crypto_config::KEY_SIZE => Ok(key), - _ => { - // Key is corrupted - regenerate - let mut key = vec![0u8; crypto_config::KEY_SIZE]; - rand::thread_rng().fill(&mut key[..]); - - let key_hex = hex_encode(&key); - keyring - .set_password(&key_hex) - .map_err(|e| format!("Failed to store key: {}", e))?; - - Ok(key) - } - } - } - Err(keyring::Error::NoEntry) => { - // No key exists yet - generate new key - let mut key = vec![0u8; crypto_config::KEY_SIZE]; - rand::thread_rng().fill(&mut key[..]); - - // Store in keychain - let key_hex = hex_encode(&key); - keyring - .set_password(&key_hex) - .map_err(|e| format!("Failed to store key: {}", e))?; - - Ok(key) - } - Err(e) => { - // Propagate other keychain errors to prevent accidental data loss - Err(format!("Failed to get key from keychain: {}", e)) - } - } - } - - /// Encrypt data with AES-256-GCM - pub fn encrypt(&self, plaintext: &[u8]) -> Result, String> { - // Generate random nonce - let mut nonce_bytes = [0u8; crypto_config::NONCE_SIZE]; - rand::thread_rng().fill(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - - // Encrypt - let ciphertext = self - .cipher - .encrypt(nonce, plaintext) - .map_err(|e| format!("Encryption failed: {}", e))?; - - // Prepend nonce to ciphertext - let mut result = Vec::with_capacity(crypto_config::NONCE_SIZE + ciphertext.len()); - result.extend_from_slice(&nonce_bytes); - result.extend_from_slice(&ciphertext); - - Ok(result) - } - - /// Decrypt data with AES-256-GCM - pub fn decrypt(&self, ciphertext: &[u8]) -> Result, String> { - if ciphertext.len() < crypto_config::NONCE_SIZE { - return Err("Ciphertext too short".to_string()); - } - - // Extract nonce and ciphertext - let nonce = Nonce::from_slice(&ciphertext[..crypto_config::NONCE_SIZE]); - let encrypted = &ciphertext[crypto_config::NONCE_SIZE..]; - - // Decrypt - self.cipher - .decrypt(nonce, encrypted) - .map_err(|e| format!("Decryption failed: {}", e)) - } -} - -/// Hex encode bytes (for keychain storage) -fn hex_encode(data: &[u8]) -> String { - let mut result = String::with_capacity(data.len() * 2); - for &byte in data { - // `format!` can't fail; avoids `unwrap()` panic paths. - result.push_str(&format!("{:02x}", byte)); - } - result -} - -/// Hex decode string to bytes -fn hex_decode(s: &str) -> Result, String> { - if s.len() % 2 != 0 { - return Err("Hex string must have even length".to_string()); - } - let mut bytes = Vec::with_capacity(s.len() / 2); - for chunk in s.as_bytes().chunks(2) { - let hex = std::str::from_utf8(chunk).map_err(|_| "Invalid UTF-8 in hex string")?; - let byte = u8::from_str_radix(hex, 16).map_err(|_| "Invalid hex character")?; - bytes.push(byte); - } - Ok(bytes) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_encrypt_decrypt() { - let crypto = CryptoBox::new().unwrap(); - let plaintext = b"Hello, World!"; - - let ciphertext = crypto.encrypt(plaintext).unwrap(); - let decrypted = crypto.decrypt(&ciphertext).unwrap(); - - assert_eq!(plaintext.as_slice(), decrypted.as_slice()); - } -} diff --git a/client/src-tauri/src/events/mod.rs b/client/src-tauri/src/events/mod.rs deleted file mode 100644 index a00c2ab..0000000 --- a/client/src-tauri/src/events/mod.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Tauri event emitters -//! -//! This module provides helper functions for emitting events -//! from the Rust backend to the React frontend. - -use serde::Serialize; -use tauri::{AppHandle, Emitter}; - -use crate::constants::events as event_names; -use crate::grpc::types::TranscriptUpdate; -use crate::state::TriggerDecision; - -/// Connection event payload -#[derive(Debug, Clone, Serialize)] -pub struct ConnectionEvent { - pub connected: bool, - pub server_address: String, - pub error: Option, -} - -/// Summary progress payload -#[derive(Debug, Clone, Serialize)] -pub struct SummaryProgress { - pub meeting_id: String, - pub status: String, - pub progress: f32, - pub error: Option, -} - -/// Diarization progress payload -#[derive(Debug, Clone, Serialize)] -pub struct DiarizationProgress { - pub job_id: String, - pub meeting_id: String, - pub status: String, - pub segments_updated: u32, - pub error: Option, -} - -/// Error event payload -#[derive(Debug, Clone, Serialize)] -pub struct ErrorEvent { - pub code: String, - pub message: String, - pub recoverable: bool, -} - -/// Emit transcript update -pub fn emit_transcript_update(app: &AppHandle, update: &TranscriptUpdate) { - let _ = app.emit(event_names::TRANSCRIPT_UPDATE, update); -} - -/// Emit audio level (normalized 0.0-1.0) -pub fn emit_audio_level(app: &AppHandle, level: f32) { - let _ = app.emit(event_names::AUDIO_LEVEL, level); -} - -/// Emit playback position -pub fn emit_playback_position(app: &AppHandle, position: f64) { - let _ = app.emit(event_names::PLAYBACK_POSITION, position); -} - -/// Emit playback state change -pub fn emit_playback_state(app: &AppHandle, state: &str) { - let _ = app.emit(event_names::PLAYBACK_STATE, state); -} - -/// Emit highlight change -pub fn emit_highlight_change(app: &AppHandle, index: Option) { - let _ = app.emit(event_names::HIGHLIGHT_CHANGE, index); -} - -/// Emit connection change -pub fn emit_connection_change(app: &AppHandle, event: &ConnectionEvent) { - let _ = app.emit(event_names::CONNECTION_CHANGE, event); -} - -/// Emit meeting detected (trigger) -pub fn emit_meeting_detected(app: &AppHandle, decision: &TriggerDecision) { - let _ = app.emit(event_names::MEETING_DETECTED, decision); -} - -/// Emit recording timer tick -pub fn emit_recording_timer(app: &AppHandle, elapsed: u32) { - let _ = app.emit(event_names::RECORDING_TIMER, elapsed); -} - -/// Emit error -pub fn emit_error(app: &AppHandle, code: &str, message: &str, recoverable: bool) { - let _ = app.emit( - event_names::ERROR, - ErrorEvent { - code: code.to_string(), - message: message.to_string(), - recoverable, - }, - ); -} diff --git a/client/src-tauri/src/grpc/client.rs b/client/src-tauri/src/grpc/client.rs deleted file mode 100644 index 99c41ba..0000000 --- a/client/src-tauri/src/grpc/client.rs +++ /dev/null @@ -1,507 +0,0 @@ -//! gRPC client implementation for NoteFlow server communication -//! -//! This module provides the `GrpcClient` struct which handles all communication -//! with the Python gRPC server using tonic. - -use std::collections::HashMap; -use std::sync::Arc; - -use parking_lot::RwLock; -use tokio::sync::Mutex; -use tonic::transport::Channel; - -use super::noteflow::{self as proto, note_flow_service_client::NoteFlowServiceClient}; -use super::types::{ - AnnotationInfo, AnnotationType, AudioChunk, DiarizationResult, ExportFormat, ExportResult, - JobStatus, MeetingDetails, MeetingInfo, MeetingState, RenameSpeakerResult, ServerInfo, - SummaryInfo, -}; -use crate::constants::grpc as grpc_config; -use crate::helpers::new_id; - -/// Convert proto Annotation to local AnnotationInfo -fn annotation_from_proto(a: proto::Annotation) -> AnnotationInfo { - AnnotationInfo { - id: a.id, - meeting_id: a.meeting_id, - annotation_type: AnnotationType::from(a.annotation_type), - text: a.text, - start_time: a.start_time, - end_time: a.end_time, - segment_ids: a.segment_ids, - created_at: a.created_at, - } -} - -/// gRPC client error type -#[derive(Debug, Clone, thiserror::Error)] -pub enum GrpcError { - #[error("Not connected to server")] - NotConnected, - #[error("No active stream")] - NoActiveStream, - #[error("Invalid address: {0}")] - InvalidAddress(String), - #[error("Connection failed: {0}")] - ConnectionFailed(String), - #[error("Operation not implemented")] - NotImplemented, -} - -impl From for String { - fn from(err: GrpcError) -> Self { - err.to_string() - } -} - -/// Connection state - grouped for atomic updates -#[derive(Debug, Clone)] -struct ConnectionState { - channel: Option, - server_address: String, - connected: bool, -} - -impl Default for ConnectionState { - fn default() -> Self { - Self { - channel: None, - server_address: String::new(), - connected: false, - } - } -} - -/// Streaming state - grouped for atomic updates -#[derive(Debug, Clone, Default)] -struct StreamingState { - meeting_id: Option, - active: bool, -} - -/// gRPC client for NoteFlow server -#[derive(Debug, Clone)] -pub struct GrpcClient { - inner: Arc, -} - -#[derive(Debug)] -struct ClientState { - /// Connection state (channel, address, connected) - single lock for atomicity - connection: RwLock, - /// Streaming state (meeting_id, active) - single lock for atomicity - streaming: RwLock, - /// Audio sender for streaming - async mutex for channel operations - audio_tx: Mutex>>, -} - -impl Default for GrpcClient { - fn default() -> Self { - Self::new() - } -} - -impl GrpcClient { - /// Create a new gRPC client - pub fn new() -> Self { - Self { - inner: Arc::new(ClientState { - connection: RwLock::new(ConnectionState::default()), - streaming: RwLock::new(StreamingState::default()), - audio_tx: Mutex::new(None), - }), - } - } - - /// Check if connected to server - pub fn is_connected(&self) -> bool { - self.inner.connection.read().connected - } - - /// Get current server address - pub fn server_address(&self) -> String { - self.inner.connection.read().server_address.clone() - } - - /// Require connection or return error - fn require_connection(&self) -> Result<(), GrpcError> { - if self.inner.connection.read().connected { - Ok(()) - } else { - Err(GrpcError::NotConnected) - } - } - - /// Get tonic client from existing channel (no new state needed) - fn tonic_client(&self) -> Result, GrpcError> { - self.inner - .connection - .read() - .channel - .clone() - .map(NoteFlowServiceClient::new) - .ok_or(GrpcError::NotConnected) - } - - /// Connect to gRPC server with timeout - pub async fn connect(&self, address: &str) -> Result { - let endpoint = Channel::from_shared(format!("http://{}", address)) - .map_err(|e| GrpcError::InvalidAddress(e.to_string()))? - .connect_timeout(grpc_config::CONNECTION_TIMEOUT) - .timeout(grpc_config::REQUEST_TIMEOUT); - - let channel = endpoint - .connect() - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - // Probe the connection before committing "connected" state. - let info = { - // Save previous state for rollback - let prev_connected; - let prev_address; - let prev_channel; - - { - let conn = self.inner.connection.read(); - prev_connected = conn.connected; - prev_address = conn.server_address.clone(); - prev_channel = conn.channel.clone(); - } - - // Atomic state update - set connected state - { - let mut conn = self.inner.connection.write(); - conn.channel = Some(channel); - conn.server_address = address.to_string(); - conn.connected = true; - } - - match self.get_server_info().await { - Ok(info) => info, - Err(e) => { - // Roll back to previous state on failure. - let mut conn = self.inner.connection.write(); - conn.connected = prev_connected; - conn.server_address = prev_address; - conn.channel = prev_channel; - return Err(e); - } - } - }; - - Ok(info) - } - - /// Disconnect from server - pub async fn disconnect(&self) { - self.stop_streaming().await; - - // Atomic state update - clear all connection state - let mut conn = self.inner.connection.write(); - conn.channel = None; - conn.server_address.clear(); - conn.connected = false; - } - - /// Get server information - pub async fn get_server_info(&self) -> Result { - self.require_connection()?; - let mut client = self.tonic_client()?; - - let response = client - .get_server_info(proto::ServerInfoRequest {}) - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - let info = response.into_inner(); - Ok(ServerInfo { - version: info.version, - asr_model: info.asr_model, - asr_ready: info.asr_ready, - uptime_seconds: info.uptime_seconds, - active_meetings: info.active_meetings as u32, - diarization_enabled: info.diarization_enabled, - diarization_ready: info.diarization_ready, - }) - } - - /// Create a new meeting - pub async fn create_meeting( - &self, - title: &str, - _metadata: Option>, - ) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(MeetingInfo::new(title)) - } - - /// Stop a recording meeting - pub async fn stop_meeting(&self, meeting_id: &str) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(MeetingInfo::stopped(meeting_id)) - } - - /// Get meeting details - pub async fn get_meeting( - &self, - meeting_id: &str, - _include_segments: bool, - _include_summary: bool, - ) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(MeetingDetails::empty(meeting_id)) - } - - /// List meetings with filtering and pagination - pub async fn list_meetings( - &self, - _states: Option>, - _limit: u32, - _offset: u32, - _sort_desc: bool, - ) -> Result<(Vec, u32), GrpcError> { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok((vec![], 0)) - } - - /// Delete a meeting - pub async fn delete_meeting(&self, _meeting_id: &str) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(true) - } - - /// Start audio streaming for a meeting - pub async fn start_streaming(&self, meeting_id: &str) -> Result<(), GrpcError> { - self.require_connection()?; - - // Atomic state update - { - let mut stream = self.inner.streaming.write(); - stream.meeting_id = Some(meeting_id.to_string()); - stream.active = true; - } - - // TODO: Implement actual gRPC streaming - Ok(()) - } - - /// Stop audio streaming - pub async fn stop_streaming(&self) { - // Clear audio sender first (async) - *self.inner.audio_tx.lock().await = None; - - // Atomic state update - let mut stream = self.inner.streaming.write(); - stream.meeting_id = None; - stream.active = false; - } - - /// Send audio chunk to stream - pub async fn send_audio(&self, _audio: Vec, _timestamp: f64) -> Result<(), GrpcError> { - if !self.inner.streaming.read().active { - return Err(GrpcError::NoActiveStream); - } - - // TODO: Implement actual audio sending - Ok(()) - } - - /// Add annotation to a meeting - pub async fn add_annotation( - &self, - meeting_id: &str, - annotation_type: AnnotationType, - text: &str, - start_time: f64, - end_time: f64, - segment_ids: Option>, - ) -> Result { - self.require_connection()?; - let mut client = self.tonic_client()?; - - let request = proto::AddAnnotationRequest { - meeting_id: meeting_id.to_string(), - annotation_type: annotation_type as i32, - text: text.to_string(), - start_time, - end_time, - segment_ids: segment_ids.unwrap_or_default(), - }; - - let response = client - .add_annotation(request) - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - Ok(annotation_from_proto(response.into_inner())) - } - - /// Get annotation by ID - pub async fn get_annotation(&self, annotation_id: &str) -> Result { - self.require_connection()?; - let mut client = self.tonic_client()?; - - let request = proto::GetAnnotationRequest { - annotation_id: annotation_id.to_string(), - }; - - let response = client - .get_annotation(request) - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - Ok(annotation_from_proto(response.into_inner())) - } - - /// List annotations for a meeting - pub async fn list_annotations( - &self, - meeting_id: &str, - start_time: f64, - end_time: f64, - ) -> Result, GrpcError> { - self.require_connection()?; - let mut client = self.tonic_client()?; - - let request = proto::ListAnnotationsRequest { - meeting_id: meeting_id.to_string(), - start_time, - end_time, - }; - - let response = client - .list_annotations(request) - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - Ok(response - .into_inner() - .annotations - .into_iter() - .map(annotation_from_proto) - .collect()) - } - - /// Update an existing annotation - pub async fn update_annotation( - &self, - annotation_id: &str, - annotation_type: Option, - text: Option<&str>, - start_time: Option, - end_time: Option, - segment_ids: Option>, - ) -> Result { - self.require_connection()?; - let mut client = self.tonic_client()?; - - let request = proto::UpdateAnnotationRequest { - annotation_id: annotation_id.to_string(), - annotation_type: annotation_type.map(|t| t as i32).unwrap_or(0), - text: text.unwrap_or("").to_string(), - start_time: start_time.unwrap_or(0.0), - end_time: end_time.unwrap_or(0.0), - segment_ids: segment_ids.unwrap_or_default(), - }; - - let response = client - .update_annotation(request) - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - Ok(annotation_from_proto(response.into_inner())) - } - - /// Delete an annotation - pub async fn delete_annotation(&self, annotation_id: &str) -> Result { - self.require_connection()?; - let mut client = self.tonic_client()?; - - let request = proto::DeleteAnnotationRequest { - annotation_id: annotation_id.to_string(), - }; - - let response = client - .delete_annotation(request) - .await - .map_err(|e| GrpcError::ConnectionFailed(e.to_string()))?; - - Ok(response.into_inner().success) - } - - /// Generate AI summary for a meeting - pub async fn generate_summary( - &self, - _meeting_id: &str, - _force_regenerate: bool, - ) -> Result { - self.require_connection()?; - Err(GrpcError::NotImplemented) - } - - /// Export meeting transcript - pub async fn export_transcript( - &self, - _meeting_id: &str, - format: ExportFormat, - ) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(ExportResult::empty(format)) - } - - /// Start background speaker diarization refinement - pub async fn refine_speaker_diarization( - &self, - _meeting_id: &str, - _num_speakers: Option, - ) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(DiarizationResult { - job_id: new_id(), - status: JobStatus::Queued, - segments_updated: 0, - speaker_ids: vec![], - error_message: String::new(), - }) - } - - /// Get status of a diarization job - pub async fn get_diarization_job_status( - &self, - _job_id: &str, - ) -> Result { - self.require_connection()?; - Err(GrpcError::NotImplemented) - } - - /// Rename a speaker across all segments - pub async fn rename_speaker( - &self, - _meeting_id: &str, - _old_speaker_id: &str, - _new_speaker_name: &str, - ) -> Result { - self.require_connection()?; - - // TODO: Implement actual gRPC call - Ok(RenameSpeakerResult { - segments_updated: 0, - success: true, - }) - } -} diff --git a/client/src-tauri/src/grpc/client_tests.rs b/client/src-tauri/src/grpc/client_tests.rs deleted file mode 100644 index bf62619..0000000 --- a/client/src-tauri/src/grpc/client_tests.rs +++ /dev/null @@ -1,206 +0,0 @@ -//! Unit tests for gRPC types -//! -//! These tests verify type construction and serialization. -//! Actual gRPC calls are tested in tests/grpc_integration.rs - -#[cfg(test)] -mod tests { - use crate::grpc::types::*; - - #[test] - fn meeting_info_new() { - let meeting = MeetingInfo::new("Test Meeting"); - - assert!(!meeting.id.is_empty()); - assert_eq!(meeting.title, "Test Meeting"); - assert_eq!(meeting.state, MeetingState::Recording); - assert!(meeting.created_at > 0.0); - } - - #[test] - fn meeting_info_stopped() { - let meeting = MeetingInfo::stopped("meeting-123"); - - assert_eq!(meeting.id, "meeting-123"); - assert_eq!(meeting.state, MeetingState::Stopped); - } - - #[test] - fn meeting_state_serialization() { - assert_eq!( - serde_json::to_string(&MeetingState::Created).unwrap(), - "\"created\"" - ); - assert_eq!( - serde_json::to_string(&MeetingState::Recording).unwrap(), - "\"recording\"" - ); - assert_eq!( - serde_json::to_string(&MeetingState::Completed).unwrap(), - "\"completed\"" - ); - assert_eq!( - serde_json::to_string(&MeetingState::Error).unwrap(), - "\"error\"" - ); - } - - #[test] - fn meeting_state_from_i32() { - assert_eq!(MeetingState::from(1), MeetingState::Created); - assert_eq!(MeetingState::from(2), MeetingState::Recording); - assert_eq!(MeetingState::from(3), MeetingState::Stopped); - assert_eq!(MeetingState::from(4), MeetingState::Completed); - assert_eq!(MeetingState::from(5), MeetingState::Error); - assert_eq!(MeetingState::from(99), MeetingState::Unspecified); - } - - #[test] - fn server_info_default() { - let info = ServerInfo::default(); - - assert!(info.version.is_empty()); - assert!(!info.asr_ready); - } - - #[test] - fn segment_construction() { - let segment = Segment { - segment_id: 1, - text: "Hello world".to_string(), - start_time: 0.0, - end_time: 5.0, - language: "en".to_string(), - speaker_id: "SPEAKER_00".to_string(), - speaker_confidence: 0.95, - words: vec![], - }; - - assert_eq!(segment.text, "Hello world"); - assert!((segment.speaker_confidence - 0.95).abs() < f64::EPSILON); - } - - #[test] - fn word_timing_construction() { - let timing = WordTiming { - word: "hello".to_string(), - start_time: 0.0, - end_time: 0.5, - probability: 0.98, - }; - - assert_eq!(timing.word, "hello"); - assert!((timing.end_time - 0.5).abs() < f64::EPSILON); - } - - #[test] - fn annotation_type_from_str() { - assert_eq!(AnnotationType::from("action_item"), AnnotationType::ActionItem); - assert_eq!(AnnotationType::from("decision"), AnnotationType::Decision); - assert_eq!(AnnotationType::from("note"), AnnotationType::Note); - assert_eq!(AnnotationType::from("risk"), AnnotationType::Risk); - assert_eq!(AnnotationType::from("unknown"), AnnotationType::Unspecified); - } - - #[test] - fn annotation_type_from_i32() { - assert_eq!(AnnotationType::from(1), AnnotationType::ActionItem); - assert_eq!(AnnotationType::from(2), AnnotationType::Decision); - assert_eq!(AnnotationType::from(3), AnnotationType::Note); - assert_eq!(AnnotationType::from(4), AnnotationType::Risk); - assert_eq!(AnnotationType::from(99), AnnotationType::Unspecified); - } - - #[test] - fn annotation_info_construction() { - let annotation = AnnotationInfo { - id: "ann-1".to_string(), - meeting_id: "meeting-123".to_string(), - annotation_type: AnnotationType::ActionItem, - text: "Follow up with team".to_string(), - start_time: 10.0, - end_time: 15.0, - segment_ids: vec![1, 2], - created_at: 1234567890.0, - }; - - assert_eq!(annotation.annotation_type, AnnotationType::ActionItem); - assert_eq!(annotation.text, "Follow up with team"); - assert_eq!(annotation.segment_ids.len(), 2); - } - - #[test] - fn timestamped_audio_construction() { - let audio = TimestampedAudio { - frames: vec![0.1, 0.2, -0.1, -0.2], - timestamp: 1234567890.0, - duration: 0.1, - }; - - assert_eq!(audio.frames.len(), 4); - assert!((audio.duration - 0.1).abs() < f64::EPSILON); - } - - #[test] - fn export_format_from_str() { - assert_eq!(ExportFormat::from("html"), ExportFormat::Html); - assert_eq!(ExportFormat::from("markdown"), ExportFormat::Markdown); - assert_eq!(ExportFormat::from("unknown"), ExportFormat::Markdown); // default - } - - #[test] - fn export_result_empty() { - let result = ExportResult::empty(ExportFormat::Markdown); - - assert!(result.content.is_empty()); - assert_eq!(result.format_name, "markdown"); - assert_eq!(result.file_extension, "md"); - - let result = ExportResult::empty(ExportFormat::Html); - assert_eq!(result.format_name, "html"); - assert_eq!(result.file_extension, "html"); - } - - #[test] - fn job_status_from_i32() { - assert_eq!(JobStatus::from(1), JobStatus::Queued); - assert_eq!(JobStatus::from(2), JobStatus::Running); - assert_eq!(JobStatus::from(3), JobStatus::Completed); - assert_eq!(JobStatus::from(4), JobStatus::Failed); - assert_eq!(JobStatus::from(99), JobStatus::Unspecified); - } - - #[test] - fn update_type_from_i32() { - assert_eq!(UpdateType::from(1), UpdateType::Partial); - assert_eq!(UpdateType::from(2), UpdateType::Final); - assert_eq!(UpdateType::from(3), UpdateType::VadStart); - assert_eq!(UpdateType::from(4), UpdateType::VadEnd); - assert_eq!(UpdateType::from(99), UpdateType::Unspecified); - } - - #[test] - fn meeting_details_empty() { - let details = MeetingDetails::empty("meeting-123"); - - assert_eq!(details.meeting.id, "meeting-123"); - assert_eq!(details.meeting.state, MeetingState::Stopped); - assert!(details.segments.is_empty()); - assert!(details.summary.is_none()); - } - - #[test] - fn audio_device_info_construction() { - let device = AudioDeviceInfo { - id: 12345, - name: "USB Microphone".to_string(), - channels: 1, - sample_rate: 48000, - is_default: true, - }; - - assert_eq!(device.id, 12345); - assert_eq!(device.name, "USB Microphone"); - assert!(device.is_default); - } -} diff --git a/client/src-tauri/src/grpc/mod.rs b/client/src-tauri/src/grpc/mod.rs deleted file mode 100644 index 54720bc..0000000 --- a/client/src-tauri/src/grpc/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! gRPC client module for NoteFlow server communication - -pub mod client; -#[allow(clippy::all)] -#[allow(dead_code)] -pub mod noteflow; -pub mod types; - -// Re-export main types -pub use client::GrpcClient; -pub use types::*; - -#[cfg(test)] -mod client_tests; diff --git a/client/src-tauri/src/grpc/noteflow.rs b/client/src-tauri/src/grpc/noteflow.rs deleted file mode 100644 index 67f51e6..0000000 --- a/client/src-tauri/src/grpc/noteflow.rs +++ /dev/null @@ -1,1205 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AudioChunk { - /// Meeting ID this audio belongs to - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Raw audio data (float32, mono, 16kHz expected) - #[prost(bytes = "vec", tag = "2")] - pub audio_data: ::prost::alloc::vec::Vec, - /// Timestamp when audio was captured (monotonic, seconds) - #[prost(double, tag = "3")] - pub timestamp: f64, - /// Sample rate in Hz (default 16000) - #[prost(int32, tag = "4")] - pub sample_rate: i32, - /// Number of channels (default 1 for mono) - #[prost(int32, tag = "5")] - pub channels: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TranscriptUpdate { - /// Meeting ID this transcript belongs to - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Type of update - #[prost(enumeration = "UpdateType", tag = "2")] - pub update_type: i32, - /// For partial updates - tentative transcript text - #[prost(string, tag = "3")] - pub partial_text: ::prost::alloc::string::String, - /// For final segments - confirmed transcript - #[prost(message, optional, tag = "4")] - pub segment: ::core::option::Option, - /// Server-side processing timestamp - #[prost(double, tag = "5")] - pub server_timestamp: f64, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct FinalSegment { - /// Segment ID (sequential within meeting) - #[prost(int32, tag = "1")] - pub segment_id: i32, - /// Transcript text - #[prost(string, tag = "2")] - pub text: ::prost::alloc::string::String, - /// Start time relative to meeting start (seconds) - #[prost(double, tag = "3")] - pub start_time: f64, - /// End time relative to meeting start (seconds) - #[prost(double, tag = "4")] - pub end_time: f64, - /// Word-level timestamps - #[prost(message, repeated, tag = "5")] - pub words: ::prost::alloc::vec::Vec, - /// Detected language - #[prost(string, tag = "6")] - pub language: ::prost::alloc::string::String, - /// Language detection confidence (0.0-1.0) - #[prost(float, tag = "7")] - pub language_confidence: f32, - /// Average log probability (quality indicator) - #[prost(float, tag = "8")] - pub avg_logprob: f32, - /// Probability that segment contains no speech - #[prost(float, tag = "9")] - pub no_speech_prob: f32, - /// Speaker identification (from diarization) - #[prost(string, tag = "10")] - pub speaker_id: ::prost::alloc::string::String, - /// Speaker assignment confidence (0.0-1.0) - #[prost(float, tag = "11")] - pub speaker_confidence: f32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct WordTiming { - #[prost(string, tag = "1")] - pub word: ::prost::alloc::string::String, - #[prost(double, tag = "2")] - pub start_time: f64, - #[prost(double, tag = "3")] - pub end_time: f64, - #[prost(float, tag = "4")] - pub probability: f32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Meeting { - /// Unique meeting identifier - #[prost(string, tag = "1")] - pub id: ::prost::alloc::string::String, - /// User-provided title - #[prost(string, tag = "2")] - pub title: ::prost::alloc::string::String, - /// Meeting state - #[prost(enumeration = "MeetingState", tag = "3")] - pub state: i32, - /// Creation timestamp (Unix epoch seconds) - #[prost(double, tag = "4")] - pub created_at: f64, - /// Start timestamp (when recording began) - #[prost(double, tag = "5")] - pub started_at: f64, - /// End timestamp (when recording stopped) - #[prost(double, tag = "6")] - pub ended_at: f64, - /// Duration in seconds - #[prost(double, tag = "7")] - pub duration_seconds: f64, - /// Full transcript segments - #[prost(message, repeated, tag = "8")] - pub segments: ::prost::alloc::vec::Vec, - /// Generated summary (if available) - #[prost(message, optional, tag = "9")] - pub summary: ::core::option::Option, - /// Metadata - #[prost(map = "string, string", tag = "10")] - pub metadata: ::std::collections::HashMap< - ::prost::alloc::string::String, - ::prost::alloc::string::String, - >, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct CreateMeetingRequest { - /// Optional title (generated if not provided) - #[prost(string, tag = "1")] - pub title: ::prost::alloc::string::String, - /// Optional metadata - #[prost(map = "string, string", tag = "2")] - pub metadata: ::std::collections::HashMap< - ::prost::alloc::string::String, - ::prost::alloc::string::String, - >, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct StopMeetingRequest { - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ListMeetingsRequest { - /// Optional filter by state - #[prost(enumeration = "MeetingState", repeated, tag = "1")] - pub states: ::prost::alloc::vec::Vec, - /// Pagination - #[prost(int32, tag = "2")] - pub limit: i32, - #[prost(int32, tag = "3")] - pub offset: i32, - /// Sort order - #[prost(enumeration = "SortOrder", tag = "4")] - pub sort_order: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ListMeetingsResponse { - #[prost(message, repeated, tag = "1")] - pub meetings: ::prost::alloc::vec::Vec, - #[prost(int32, tag = "2")] - pub total_count: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetMeetingRequest { - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Whether to include full transcript segments - #[prost(bool, tag = "2")] - pub include_segments: bool, - /// Whether to include summary - #[prost(bool, tag = "3")] - pub include_summary: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DeleteMeetingRequest { - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct DeleteMeetingResponse { - #[prost(bool, tag = "1")] - pub success: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Summary { - /// Meeting this summary belongs to - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Executive summary (2-3 sentences) - #[prost(string, tag = "2")] - pub executive_summary: ::prost::alloc::string::String, - /// Key points / highlights - #[prost(message, repeated, tag = "3")] - pub key_points: ::prost::alloc::vec::Vec, - /// Action items extracted - #[prost(message, repeated, tag = "4")] - pub action_items: ::prost::alloc::vec::Vec, - /// Generated timestamp - #[prost(double, tag = "5")] - pub generated_at: f64, - /// Model/version used for generation - #[prost(string, tag = "6")] - pub model_version: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct KeyPoint { - /// The key point text - #[prost(string, tag = "1")] - pub text: ::prost::alloc::string::String, - /// Segment IDs that support this point (evidence linking) - #[prost(int32, repeated, tag = "2")] - pub segment_ids: ::prost::alloc::vec::Vec, - /// Timestamp range this point covers - #[prost(double, tag = "3")] - pub start_time: f64, - #[prost(double, tag = "4")] - pub end_time: f64, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ActionItem { - /// Action item text - #[prost(string, tag = "1")] - pub text: ::prost::alloc::string::String, - /// Assigned to (if mentioned) - #[prost(string, tag = "2")] - pub assignee: ::prost::alloc::string::String, - /// Due date (if mentioned, Unix epoch) - #[prost(double, tag = "3")] - pub due_date: f64, - /// Priority level - #[prost(enumeration = "Priority", tag = "4")] - pub priority: i32, - /// Segment IDs that mention this action - #[prost(int32, repeated, tag = "5")] - pub segment_ids: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GenerateSummaryRequest { - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Force regeneration even if summary exists - #[prost(bool, tag = "2")] - pub force_regenerate: bool, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct ServerInfoRequest {} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ServerInfo { - /// Server version - #[prost(string, tag = "1")] - pub version: ::prost::alloc::string::String, - /// ASR model loaded - #[prost(string, tag = "2")] - pub asr_model: ::prost::alloc::string::String, - /// Whether ASR is ready - #[prost(bool, tag = "3")] - pub asr_ready: bool, - /// Supported sample rates - #[prost(int32, repeated, tag = "4")] - pub supported_sample_rates: ::prost::alloc::vec::Vec, - /// Maximum audio chunk size in bytes - #[prost(int32, tag = "5")] - pub max_chunk_size: i32, - /// Server uptime in seconds - #[prost(double, tag = "6")] - pub uptime_seconds: f64, - /// Number of active meetings - #[prost(int32, tag = "7")] - pub active_meetings: i32, - /// Whether diarization is enabled - #[prost(bool, tag = "8")] - pub diarization_enabled: bool, - /// Whether diarization models are ready - #[prost(bool, tag = "9")] - pub diarization_ready: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Annotation { - /// Unique annotation identifier - #[prost(string, tag = "1")] - pub id: ::prost::alloc::string::String, - /// Meeting this annotation belongs to - #[prost(string, tag = "2")] - pub meeting_id: ::prost::alloc::string::String, - /// Type of annotation - #[prost(enumeration = "AnnotationType", tag = "3")] - pub annotation_type: i32, - /// Annotation text - #[prost(string, tag = "4")] - pub text: ::prost::alloc::string::String, - /// Start time relative to meeting start (seconds) - #[prost(double, tag = "5")] - pub start_time: f64, - /// End time relative to meeting start (seconds) - #[prost(double, tag = "6")] - pub end_time: f64, - /// Linked segment IDs (evidence linking) - #[prost(int32, repeated, tag = "7")] - pub segment_ids: ::prost::alloc::vec::Vec, - /// Creation timestamp (Unix epoch seconds) - #[prost(double, tag = "8")] - pub created_at: f64, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AddAnnotationRequest { - /// Meeting ID to add annotation to - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Type of annotation - #[prost(enumeration = "AnnotationType", tag = "2")] - pub annotation_type: i32, - /// Annotation text - #[prost(string, tag = "3")] - pub text: ::prost::alloc::string::String, - /// Start time relative to meeting start (seconds) - #[prost(double, tag = "4")] - pub start_time: f64, - /// End time relative to meeting start (seconds) - #[prost(double, tag = "5")] - pub end_time: f64, - /// Optional linked segment IDs - #[prost(int32, repeated, tag = "6")] - pub segment_ids: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetAnnotationRequest { - #[prost(string, tag = "1")] - pub annotation_id: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ListAnnotationsRequest { - /// Meeting ID to list annotations for - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Optional time range filter - #[prost(double, tag = "2")] - pub start_time: f64, - #[prost(double, tag = "3")] - pub end_time: f64, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ListAnnotationsResponse { - #[prost(message, repeated, tag = "1")] - pub annotations: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct UpdateAnnotationRequest { - /// Annotation ID to update - #[prost(string, tag = "1")] - pub annotation_id: ::prost::alloc::string::String, - /// Updated type (optional, keeps existing if not set) - #[prost(enumeration = "AnnotationType", tag = "2")] - pub annotation_type: i32, - /// Updated text (optional, keeps existing if empty) - #[prost(string, tag = "3")] - pub text: ::prost::alloc::string::String, - /// Updated start time (optional, keeps existing if 0) - #[prost(double, tag = "4")] - pub start_time: f64, - /// Updated end time (optional, keeps existing if 0) - #[prost(double, tag = "5")] - pub end_time: f64, - /// Updated segment IDs (replaces existing) - #[prost(int32, repeated, tag = "6")] - pub segment_ids: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DeleteAnnotationRequest { - #[prost(string, tag = "1")] - pub annotation_id: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct DeleteAnnotationResponse { - #[prost(bool, tag = "1")] - pub success: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ExportTranscriptRequest { - /// Meeting ID to export - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Export format - #[prost(enumeration = "ExportFormat", tag = "2")] - pub format: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ExportTranscriptResponse { - /// Exported content - #[prost(string, tag = "1")] - pub content: ::prost::alloc::string::String, - /// Format name - #[prost(string, tag = "2")] - pub format_name: ::prost::alloc::string::String, - /// Suggested file extension - #[prost(string, tag = "3")] - pub file_extension: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct RefineSpeakerDiarizationRequest { - /// Meeting ID to run diarization on - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Optional known number of speakers (auto-detect if not set or 0) - #[prost(int32, tag = "2")] - pub num_speakers: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct RefineSpeakerDiarizationResponse { - /// Number of segments updated with speaker labels - #[prost(int32, tag = "1")] - pub segments_updated: i32, - /// Distinct speaker IDs found - #[prost(string, repeated, tag = "2")] - pub speaker_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - /// Error message if diarization failed - #[prost(string, tag = "3")] - pub error_message: ::prost::alloc::string::String, - /// Background job identifier (empty if request failed) - #[prost(string, tag = "4")] - pub job_id: ::prost::alloc::string::String, - /// Current job status - #[prost(enumeration = "JobStatus", tag = "5")] - pub status: i32, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct RenameSpeakerRequest { - /// Meeting ID - #[prost(string, tag = "1")] - pub meeting_id: ::prost::alloc::string::String, - /// Original speaker ID (e.g., "SPEAKER_00") - #[prost(string, tag = "2")] - pub old_speaker_id: ::prost::alloc::string::String, - /// New speaker name (e.g., "Alice") - #[prost(string, tag = "3")] - pub new_speaker_name: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct RenameSpeakerResponse { - /// Number of segments updated - #[prost(int32, tag = "1")] - pub segments_updated: i32, - /// Success flag - #[prost(bool, tag = "2")] - pub success: bool, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetDiarizationJobStatusRequest { - /// Job ID returned by RefineSpeakerDiarization - #[prost(string, tag = "1")] - pub job_id: ::prost::alloc::string::String, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DiarizationJobStatus { - /// Job ID - #[prost(string, tag = "1")] - pub job_id: ::prost::alloc::string::String, - /// Current status - #[prost(enumeration = "JobStatus", tag = "2")] - pub status: i32, - /// Number of segments updated (when completed) - #[prost(int32, tag = "3")] - pub segments_updated: i32, - /// Distinct speaker IDs found (when completed) - #[prost(string, repeated, tag = "4")] - pub speaker_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - /// Error message if failed - #[prost(string, tag = "5")] - pub error_message: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum UpdateType { - Unspecified = 0, - /// Tentative, may change - Partial = 1, - /// Confirmed segment - Final = 2, - /// Voice activity started - VadStart = 3, - /// Voice activity ended - VadEnd = 4, -} -impl UpdateType { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "UPDATE_TYPE_UNSPECIFIED", - Self::Partial => "UPDATE_TYPE_PARTIAL", - Self::Final => "UPDATE_TYPE_FINAL", - Self::VadStart => "UPDATE_TYPE_VAD_START", - Self::VadEnd => "UPDATE_TYPE_VAD_END", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "UPDATE_TYPE_UNSPECIFIED" => Some(Self::Unspecified), - "UPDATE_TYPE_PARTIAL" => Some(Self::Partial), - "UPDATE_TYPE_FINAL" => Some(Self::Final), - "UPDATE_TYPE_VAD_START" => Some(Self::VadStart), - "UPDATE_TYPE_VAD_END" => Some(Self::VadEnd), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum MeetingState { - Unspecified = 0, - /// Created but not started - Created = 1, - /// Actively recording - Recording = 2, - /// Recording stopped, processing may continue - Stopped = 3, - /// All processing complete - Completed = 4, - /// Error occurred - Error = 5, -} -impl MeetingState { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "MEETING_STATE_UNSPECIFIED", - Self::Created => "MEETING_STATE_CREATED", - Self::Recording => "MEETING_STATE_RECORDING", - Self::Stopped => "MEETING_STATE_STOPPED", - Self::Completed => "MEETING_STATE_COMPLETED", - Self::Error => "MEETING_STATE_ERROR", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "MEETING_STATE_UNSPECIFIED" => Some(Self::Unspecified), - "MEETING_STATE_CREATED" => Some(Self::Created), - "MEETING_STATE_RECORDING" => Some(Self::Recording), - "MEETING_STATE_STOPPED" => Some(Self::Stopped), - "MEETING_STATE_COMPLETED" => Some(Self::Completed), - "MEETING_STATE_ERROR" => Some(Self::Error), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum SortOrder { - Unspecified = 0, - /// Newest first (default) - CreatedDesc = 1, - /// Oldest first - CreatedAsc = 2, -} -impl SortOrder { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "SORT_ORDER_UNSPECIFIED", - Self::CreatedDesc => "SORT_ORDER_CREATED_DESC", - Self::CreatedAsc => "SORT_ORDER_CREATED_ASC", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "SORT_ORDER_UNSPECIFIED" => Some(Self::Unspecified), - "SORT_ORDER_CREATED_DESC" => Some(Self::CreatedDesc), - "SORT_ORDER_CREATED_ASC" => Some(Self::CreatedAsc), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum Priority { - Unspecified = 0, - Low = 1, - Medium = 2, - High = 3, -} -impl Priority { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "PRIORITY_UNSPECIFIED", - Self::Low => "PRIORITY_LOW", - Self::Medium => "PRIORITY_MEDIUM", - Self::High => "PRIORITY_HIGH", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "PRIORITY_UNSPECIFIED" => Some(Self::Unspecified), - "PRIORITY_LOW" => Some(Self::Low), - "PRIORITY_MEDIUM" => Some(Self::Medium), - "PRIORITY_HIGH" => Some(Self::High), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum AnnotationType { - Unspecified = 0, - ActionItem = 1, - Decision = 2, - Note = 3, - Risk = 4, -} -impl AnnotationType { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "ANNOTATION_TYPE_UNSPECIFIED", - Self::ActionItem => "ANNOTATION_TYPE_ACTION_ITEM", - Self::Decision => "ANNOTATION_TYPE_DECISION", - Self::Note => "ANNOTATION_TYPE_NOTE", - Self::Risk => "ANNOTATION_TYPE_RISK", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "ANNOTATION_TYPE_UNSPECIFIED" => Some(Self::Unspecified), - "ANNOTATION_TYPE_ACTION_ITEM" => Some(Self::ActionItem), - "ANNOTATION_TYPE_DECISION" => Some(Self::Decision), - "ANNOTATION_TYPE_NOTE" => Some(Self::Note), - "ANNOTATION_TYPE_RISK" => Some(Self::Risk), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum ExportFormat { - Unspecified = 0, - Markdown = 1, - Html = 2, -} -impl ExportFormat { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "EXPORT_FORMAT_UNSPECIFIED", - Self::Markdown => "EXPORT_FORMAT_MARKDOWN", - Self::Html => "EXPORT_FORMAT_HTML", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "EXPORT_FORMAT_UNSPECIFIED" => Some(Self::Unspecified), - "EXPORT_FORMAT_MARKDOWN" => Some(Self::Markdown), - "EXPORT_FORMAT_HTML" => Some(Self::Html), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum JobStatus { - Unspecified = 0, - Queued = 1, - Running = 2, - Completed = 3, - Failed = 4, -} -impl JobStatus { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unspecified => "JOB_STATUS_UNSPECIFIED", - Self::Queued => "JOB_STATUS_QUEUED", - Self::Running => "JOB_STATUS_RUNNING", - Self::Completed => "JOB_STATUS_COMPLETED", - Self::Failed => "JOB_STATUS_FAILED", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "JOB_STATUS_UNSPECIFIED" => Some(Self::Unspecified), - "JOB_STATUS_QUEUED" => Some(Self::Queued), - "JOB_STATUS_RUNNING" => Some(Self::Running), - "JOB_STATUS_COMPLETED" => Some(Self::Completed), - "JOB_STATUS_FAILED" => Some(Self::Failed), - _ => None, - } - } -} -/// Generated client implementations. -pub mod note_flow_service_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct NoteFlowServiceClient { - inner: tonic::client::Grpc, - } - impl NoteFlowServiceClient { - /// Attempt to create a new client by connecting to a given endpoint. - pub async fn connect(dst: D) -> Result - where - D: TryInto, - D::Error: Into, - { - let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; - Ok(Self::new(conn)) - } - } - impl NoteFlowServiceClient - where - T: tonic::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tonic::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tonic::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> NoteFlowServiceClient> - where - F: tonic::service::Interceptor, - T::ResponseBody: Default, - T: tonic::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - NoteFlowServiceClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// Bidirectional streaming: client sends audio chunks, server returns transcripts - pub async fn stream_transcription( - &mut self, - request: impl tonic::IntoStreamingRequest, - ) -> std::result::Result< - tonic::Response>, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/StreamTranscription", - ); - let mut req = request.into_streaming_request(); - req.extensions_mut() - .insert( - GrpcMethod::new("noteflow.NoteFlowService", "StreamTranscription"), - ); - self.inner.streaming(req, path, codec).await - } - /// Meeting lifecycle management - pub async fn create_meeting( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/CreateMeeting", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "CreateMeeting")); - self.inner.unary(req, path, codec).await - } - pub async fn stop_meeting( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/StopMeeting", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "StopMeeting")); - self.inner.unary(req, path, codec).await - } - pub async fn list_meetings( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/ListMeetings", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "ListMeetings")); - self.inner.unary(req, path, codec).await - } - pub async fn get_meeting( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/GetMeeting", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "GetMeeting")); - self.inner.unary(req, path, codec).await - } - pub async fn delete_meeting( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/DeleteMeeting", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "DeleteMeeting")); - self.inner.unary(req, path, codec).await - } - /// Summary generation - pub async fn generate_summary( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/GenerateSummary", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "GenerateSummary")); - self.inner.unary(req, path, codec).await - } - /// Annotation management - pub async fn add_annotation( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/AddAnnotation", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "AddAnnotation")); - self.inner.unary(req, path, codec).await - } - pub async fn get_annotation( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/GetAnnotation", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "GetAnnotation")); - self.inner.unary(req, path, codec).await - } - pub async fn list_annotations( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/ListAnnotations", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "ListAnnotations")); - self.inner.unary(req, path, codec).await - } - pub async fn update_annotation( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/UpdateAnnotation", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "UpdateAnnotation")); - self.inner.unary(req, path, codec).await - } - pub async fn delete_annotation( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/DeleteAnnotation", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "DeleteAnnotation")); - self.inner.unary(req, path, codec).await - } - /// Export functionality - pub async fn export_transcript( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/ExportTranscript", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "ExportTranscript")); - self.inner.unary(req, path, codec).await - } - /// Speaker diarization - pub async fn refine_speaker_diarization( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/RefineSpeakerDiarization", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "noteflow.NoteFlowService", - "RefineSpeakerDiarization", - ), - ); - self.inner.unary(req, path, codec).await - } - pub async fn rename_speaker( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/RenameSpeaker", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "RenameSpeaker")); - self.inner.unary(req, path, codec).await - } - pub async fn get_diarization_job_status( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/GetDiarizationJobStatus", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "noteflow.NoteFlowService", - "GetDiarizationJobStatus", - ), - ); - self.inner.unary(req, path, codec).await - } - /// Server health and capabilities - pub async fn get_server_info( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/noteflow.NoteFlowService/GetServerInfo", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("noteflow.NoteFlowService", "GetServerInfo")); - self.inner.unary(req, path, codec).await - } - } -} diff --git a/client/src-tauri/src/grpc/types.rs b/client/src-tauri/src/grpc/types.rs deleted file mode 100644 index 5393ae3..0000000 --- a/client/src-tauri/src/grpc/types.rs +++ /dev/null @@ -1,357 +0,0 @@ -//! Type definitions for gRPC messages -//! -//! These types mirror the protobuf definitions and are used for -//! communication between Rust and the React frontend via Tauri. - -use serde::{Deserialize, Serialize}; - -use crate::helpers::{new_id, now_timestamp}; - -/// Meeting states (matches proto enum) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum MeetingState { - #[default] - Unspecified = 0, - Created = 1, - Recording = 2, - Stopped = 3, - Completed = 4, - Error = 5, - Stopping = 6, -} - -impl From for MeetingState { - fn from(value: i32) -> Self { - match value { - 1 => Self::Created, - 2 => Self::Recording, - 3 => Self::Stopped, - 4 => Self::Completed, - 5 => Self::Error, - 6 => Self::Stopping, - _ => Self::Unspecified, - } - } -} - -/// Annotation types (matches proto enum) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum AnnotationType { - #[default] - Unspecified = 0, - ActionItem = 1, - Decision = 2, - Note = 3, - Risk = 4, -} - -impl From<&str> for AnnotationType { - fn from(value: &str) -> Self { - match value { - "action_item" => Self::ActionItem, - "decision" => Self::Decision, - "note" => Self::Note, - "risk" => Self::Risk, - _ => Self::Unspecified, - } - } -} - -impl From for AnnotationType { - fn from(value: i32) -> Self { - match value { - 1 => Self::ActionItem, - 2 => Self::Decision, - 3 => Self::Note, - 4 => Self::Risk, - _ => Self::Unspecified, - } - } -} - -/// Export formats (matches proto enum) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum ExportFormat { - #[default] - Markdown = 0, - Html = 1, -} - -impl From<&str> for ExportFormat { - fn from(value: &str) -> Self { - match value { - "html" => Self::Html, - _ => Self::Markdown, - } - } -} - -/// Job status for background tasks (matches proto enum) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum JobStatus { - #[default] - Unspecified = 0, - Queued = 1, - Running = 2, - Completed = 3, - Failed = 4, -} - -impl From for JobStatus { - fn from(value: i32) -> Self { - match value { - 1 => Self::Queued, - 2 => Self::Running, - 3 => Self::Completed, - 4 => Self::Failed, - _ => Self::Unspecified, - } - } -} - -/// Transcript update type (matches proto enum) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum UpdateType { - #[default] - Unspecified = 0, - Partial = 1, - Final = 2, - VadStart = 3, - VadEnd = 4, -} - -impl From for UpdateType { - fn from(value: i32) -> Self { - match value { - 1 => Self::Partial, - 2 => Self::Final, - 3 => Self::VadStart, - 4 => Self::VadEnd, - _ => Self::Unspecified, - } - } -} - -/// Server info (returned from GetServerInfo) -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ServerInfo { - pub version: String, - pub asr_model: String, - pub asr_ready: bool, - pub uptime_seconds: f64, - pub active_meetings: u32, - pub diarization_enabled: bool, - pub diarization_ready: bool, -} - -/// Meeting info (basic meeting data) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MeetingInfo { - pub id: String, - pub title: String, - pub state: MeetingState, - pub created_at: f64, - pub started_at: f64, - pub ended_at: f64, - pub duration_seconds: f64, - pub segment_count: u32, -} - -impl MeetingInfo { - /// Create a new meeting with the given title - pub fn new(title: &str) -> Self { - let now = now_timestamp(); - Self { - id: new_id(), - title: title.to_string(), - state: MeetingState::Recording, - created_at: now, - started_at: now, - ended_at: 0.0, - duration_seconds: 0.0, - segment_count: 0, - } - } - - /// Create a stopped meeting placeholder - pub fn stopped(meeting_id: &str) -> Self { - Self { - id: meeting_id.to_string(), - title: String::new(), - state: MeetingState::Stopped, - created_at: 0.0, - started_at: 0.0, - ended_at: now_timestamp(), - duration_seconds: 0.0, - segment_count: 0, - } - } -} - -/// Meeting with segments and summary -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MeetingDetails { - pub meeting: MeetingInfo, - pub segments: Vec, - pub summary: Option, -} - -impl MeetingDetails { - /// Create an empty meeting details placeholder - pub fn empty(meeting_id: &str) -> Self { - Self { - meeting: MeetingInfo::stopped(meeting_id), - segments: vec![], - summary: None, - } - } -} - -/// Transcript segment -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Segment { - pub segment_id: i32, - pub text: String, - pub start_time: f64, - pub end_time: f64, - pub language: String, - pub speaker_id: String, - pub speaker_confidence: f64, - pub words: Vec, -} - -/// Word-level timing -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WordTiming { - pub word: String, - pub start_time: f64, - pub end_time: f64, - pub probability: f64, -} - -/// Transcript update from streaming -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TranscriptUpdate { - pub update_type: UpdateType, - pub partial_text: String, - pub segment: Option, - pub server_timestamp: f64, -} - -/// Annotation info -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AnnotationInfo { - pub id: String, - pub meeting_id: String, - pub annotation_type: AnnotationType, - pub text: String, - pub start_time: f64, - pub end_time: f64, - pub segment_ids: Vec, - pub created_at: f64, -} - - -/// Summary info -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SummaryInfo { - pub meeting_id: String, - pub executive_summary: String, - pub key_points: Vec, - pub action_items: Vec, - pub generated_at: f64, - pub model_version: String, -} - -/// Key point from summary -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KeyPoint { - pub text: String, - pub segment_ids: Vec, - pub start_time: f64, - pub end_time: f64, -} - -/// Action item from summary -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ActionItem { - pub text: String, - pub assignee: String, - pub due_date: Option, - pub priority: u32, - pub segment_ids: Vec, -} - -/// Export result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ExportResult { - pub content: String, - pub format_name: String, - pub file_extension: String, -} - -impl ExportResult { - /// Create an empty export result placeholder - pub fn empty(format: ExportFormat) -> Self { - let (format_name, file_extension) = match format { - ExportFormat::Markdown => ("markdown", "md"), - ExportFormat::Html => ("html", "html"), - }; - - Self { - content: String::new(), - format_name: format_name.to_string(), - file_extension: file_extension.to_string(), - } - } -} - -/// Diarization job result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiarizationResult { - pub job_id: String, - pub status: JobStatus, - pub segments_updated: u32, - pub speaker_ids: Vec, - pub error_message: String, -} - -/// Speaker rename result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RenameSpeakerResult { - pub segments_updated: u32, - pub success: bool, -} - -/// Audio device info -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AudioDeviceInfo { - pub id: u32, - pub name: String, - pub channels: u32, - pub sample_rate: u32, - pub is_default: bool, -} - -/// Audio chunk for streaming -#[derive(Debug, Clone)] -pub struct AudioChunk { - pub meeting_id: String, - pub audio_data: Vec, - pub timestamp: f64, - pub sample_rate: u32, - pub channels: u32, -} - -/// Timestamped audio for playback buffer -#[derive(Debug, Clone)] -pub struct TimestampedAudio { - pub frames: Vec, - pub timestamp: f64, - pub duration: f64, -} diff --git a/client/src-tauri/src/helpers.rs b/client/src-tauri/src/helpers.rs deleted file mode 100644 index 5a72019..0000000 --- a/client/src-tauri/src/helpers.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Common utility functions -//! -//! Centralized helpers used across the application to avoid code duplication. - -use std::time::SystemTime; - -/// Get current Unix timestamp as f64 seconds -/// -/// Returns seconds since Unix epoch with sub-second precision. -/// Falls back to 0.0 if system time is before epoch (shouldn't happen). -#[inline] -pub fn now_timestamp() -> f64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map(|d| d.as_secs_f64()) - .unwrap_or(0.0) -} - -/// Generate a new UUID v4 string -/// -/// Returns a lowercase hyphenated UUID string. -#[inline] -pub fn new_id() -> String { - uuid::Uuid::new_v4().to_string() -} - -/// Format duration in seconds to human-readable string -/// -/// Returns format like "1:23" for short durations or "1:23:45" for longer ones. -/// Handles non-finite or negative values by returning "0:00". -pub fn format_duration(seconds: f64) -> String { - // Guard against non-finite or negative values - if !seconds.is_finite() || seconds < 0.0 { - return "0:00".to_string(); - } - - let total_secs = seconds.floor() as u64; - let hours = total_secs / 3600; - let minutes = (total_secs % 3600) / 60; - let secs = total_secs % 60; - - if hours > 0 { - format!("{}:{:02}:{:02}", hours, minutes, secs) - } else { - format!("{}:{:02}", minutes, secs) - } -} - -/// Sanitize a filename by replacing invalid characters -/// -/// Replaces characters not allowed in filenames with underscores. -pub fn sanitize_filename(name: &str) -> String { - name.chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_', - _ => c, - }) - .collect() -} - -/// Clamp a value between min and max -#[inline] -pub fn clamp(value: T, min: T, max: T) -> T { - if value < min { - min - } else if value > max { - max - } else { - value - } -} - -/// Normalize audio level from dB to 0.0-1.0 range -/// -/// Returns 0.0 if min_db == max_db to avoid division by zero. -#[inline] -pub fn normalize_db_level(db: f32, min_db: f32, max_db: f32) -> f32 { - let range = max_db - min_db; - if range.abs() < f32::EPSILON { - return 0.0; - } - clamp((db - min_db) / range, 0.0, 1.0) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn now_timestamp_is_positive() { - let ts = now_timestamp(); - assert!(ts > 0.0); - // Should be after year 2020 (timestamp > 1577836800) - assert!(ts > 1577836800.0); - } - - #[test] - fn new_id_is_valid_uuid() { - let id = new_id(); - // UUID v4 format: 8-4-4-4-12 characters - assert_eq!(id.len(), 36); - assert!(uuid::Uuid::parse_str(&id).is_ok()); - } - - #[test] - fn new_id_is_unique() { - let id1 = new_id(); - let id2 = new_id(); - assert_ne!(id1, id2); - } - - #[test] - fn format_duration_short() { - assert_eq!(format_duration(0.0), "0:00"); - assert_eq!(format_duration(5.0), "0:05"); - assert_eq!(format_duration(65.0), "1:05"); - assert_eq!(format_duration(599.0), "9:59"); - } - - #[test] - fn format_duration_long() { - assert_eq!(format_duration(3600.0), "1:00:00"); - assert_eq!(format_duration(3661.0), "1:01:01"); - assert_eq!(format_duration(7325.0), "2:02:05"); - } - - #[test] - fn sanitize_filename_removes_invalid_chars() { - assert_eq!(sanitize_filename("test/file"), "test_file"); - assert_eq!(sanitize_filename("a:b*c?d"), "a_b_c_d"); - assert_eq!(sanitize_filename("normal name"), "normal name"); - assert_eq!(sanitize_filename("file<>name"), "file__name"); - } - - #[test] - fn clamp_works_correctly() { - assert_eq!(clamp(5, 0, 10), 5); - assert_eq!(clamp(-5, 0, 10), 0); - assert_eq!(clamp(15, 0, 10), 10); - } - - #[test] - fn normalize_db_level_works() { - assert_eq!(normalize_db_level(-60.0, -60.0, 0.0), 0.0); - assert_eq!(normalize_db_level(0.0, -60.0, 0.0), 1.0); - assert_eq!(normalize_db_level(-30.0, -60.0, 0.0), 0.5); - // Clamped values - assert_eq!(normalize_db_level(-100.0, -60.0, 0.0), 0.0); - assert_eq!(normalize_db_level(10.0, -60.0, 0.0), 1.0); - } - - #[test] - fn normalize_db_level_handles_zero_range() { - // Should return 0.0 when min == max to avoid division by zero - assert_eq!(normalize_db_level(0.0, 0.0, 0.0), 0.0); - assert_eq!(normalize_db_level(-60.0, -60.0, -60.0), 0.0); - } -} diff --git a/client/src-tauri/src/lib.rs b/client/src-tauri/src/lib.rs deleted file mode 100644 index bccee47..0000000 --- a/client/src-tauri/src/lib.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! NoteFlow Tauri Client Library -//! -//! This library implements the Tauri backend for the NoteFlow meeting notetaker. -//! It provides gRPC connectivity, audio capture/playback, and system integration. - -use std::sync::Arc; -use tauri::Manager; - -pub mod audio; -pub mod cache; -pub mod commands; -pub mod config; -pub mod constants; -pub mod crypto; -pub mod events; -pub mod grpc; -pub mod helpers; -pub mod state; -pub mod triggers; - -use commands::preferences::load_preferences_from_disk; -use crypto::CryptoBox; -use state::AppState; - -/// Initialize and run the Tauri application -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tracing_subscriber::fmt::init(); - - tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .plugin(tauri_plugin_window_state::Builder::default().build()) - .setup(|app| { - // Initialize crypto - let crypto = Arc::new(CryptoBox::new().map_err(|e| { - tauri::Error::Setup( - Box::::from(format!( - "Failed to initialize crypto: {}", - e - )) - .into(), - ) - })?); - - // Load preferences from disk - let prefs = load_preferences_from_disk(); - - // Create state with loaded preferences - let state = Arc::new(AppState::new_with_preferences(crypto, prefs)); - app.manage(state); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - // Connection - commands::connection::connect, - commands::connection::disconnect, - commands::connection::get_server_info, - commands::connection::get_status, - // Recording - commands::recording::start_recording, - commands::recording::stop_recording, - // Meetings - commands::meeting::list_meetings, - commands::meeting::get_meeting, - commands::meeting::delete_meeting, - commands::meeting::select_meeting, - // Annotations - commands::annotation::add_annotation, - commands::annotation::get_annotation, - commands::annotation::list_annotations, - commands::annotation::update_annotation, - commands::annotation::delete_annotation, - // Summary - commands::summary::generate_summary, - // Export - commands::export::export_transcript, - commands::export::save_export_file, - // Diarization - commands::diarization::refine_speaker_diarization, - commands::diarization::get_diarization_job_status, - commands::diarization::rename_speaker, - // Playback - commands::playback::play, - commands::playback::pause, - commands::playback::stop, - commands::playback::seek, - commands::playback::get_playback_state, - // Triggers - commands::triggers::set_trigger_enabled, - commands::triggers::snooze_triggers, - commands::triggers::reset_snooze, - commands::triggers::get_trigger_status, - commands::triggers::dismiss_trigger, - commands::triggers::accept_trigger, - // Audio devices - commands::audio::list_audio_devices, - commands::audio::select_audio_device, - commands::audio::get_current_device, - // Preferences - commands::preferences::get_preferences, - commands::preferences::save_preferences, - ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} diff --git a/client/src-tauri/src/main.rs b/client/src-tauri/src/main.rs deleted file mode 100644 index f6fad2a..0000000 --- a/client/src-tauri/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - noteflow_tauri_lib::run(); -} diff --git a/client/src-tauri/src/state/app_state.rs b/client/src-tauri/src/state/app_state.rs deleted file mode 100644 index 89900d3..0000000 --- a/client/src-tauri/src/state/app_state.rs +++ /dev/null @@ -1,406 +0,0 @@ -//! Central application state -//! -//! This module defines `AppState` which mirrors the Python `AppState` dataclass -//! and provides thread-safe access to all application state. - -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; - -use parking_lot::RwLock; -use serde::Serialize; -use tokio::sync::Mutex; - -use crate::audio::PlaybackHandle; -use crate::constants::audio::DEFAULT_SAMPLE_RATE; -use crate::crypto::CryptoBox; -use crate::grpc::client::GrpcClient; -use crate::grpc::types::*; -use crate::triggers::TriggerService; - -/// Playback state machine -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum PlaybackState { - #[default] - Stopped, - Playing, - Paused, -} - -/// Trigger source -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum TriggerSource { - AudioActivity, - ForegroundApp, - Calendar, -} - -/// Trigger action -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum TriggerAction { - Ignore, - Notify, - AutoStart, -} - -/// Individual trigger signal -#[derive(Debug, Clone, Serialize)] -pub struct TriggerSignal { - pub source: TriggerSource, - pub weight: f32, - pub app_name: Option, - pub timestamp: f64, -} - -/// Trigger decision result -#[derive(Debug, Clone, Serialize)] -pub struct TriggerDecision { - pub action: TriggerAction, - pub confidence: f32, - pub signals: Vec, - pub timestamp: f64, - pub detected_app: Option, -} - -/// Central application state -/// -/// All state is managed here for component access. -/// Wrapped in `tauri::State>` for command handlers. -/// -/// Note: Connection state (connected, server_address) is managed by GrpcClient -/// as the single source of truth. AppState only caches server_info for convenience. -pub struct AppState { - // ========================================================================= - // Connection State - // ========================================================================= - /// gRPC client (shared across commands) - source of truth for connection state - pub grpc_client: GrpcClient, - - /// Server information cache (after connect) - pub server_info: RwLock>, - - // ========================================================================= - // Recording State - // ========================================================================= - /// Recording active flag - pub recording: RwLock, - - /// Current meeting being recorded - pub current_meeting: RwLock>, - - /// Recording start timestamp (Unix epoch) - pub recording_start_time: RwLock>, - - /// Elapsed recording seconds (for timer display) - pub elapsed_seconds: RwLock, - - // ========================================================================= - // Audio Capture State - // ========================================================================= - /// Current audio level in dB (-60 to 0) - pub current_db_level: RwLock, - - /// Audio level normalized (0.0 to 1.0) - pub current_level_normalized: RwLock, - - // ========================================================================= - // Transcript State - // ========================================================================= - /// Final transcript segments (ordered by segment_id) - pub transcript_segments: RwLock>, - - /// Current partial text (live, not yet final) - pub current_partial_text: RwLock, - - // ========================================================================= - // Playback State - // ========================================================================= - /// Audio playback handle (channel-based, thread-safe) - pub audio_playback: RwLock>, - - /// Playback state machine - pub playback_state: RwLock, - - /// Current playback position in seconds - pub playback_position: RwLock, - - /// Total playback duration in seconds - pub playback_duration: RwLock, - - /// Sample rate for current playback buffer - pub playback_sample_rate: RwLock, - - /// Accumulated samples played (for resume tracking) - pub playback_samples_played: RwLock, - - /// Session audio buffer (for playback after recording) - pub session_audio_buffer: RwLock>, - - // ========================================================================= - // Transcript Sync State - // ========================================================================= - /// Currently highlighted segment index (for playback sync) - pub highlighted_segment_index: RwLock>, - - // ========================================================================= - // Annotations State - // ========================================================================= - /// Annotations for current meeting - pub annotations: RwLock>, - - // ========================================================================= - // Meeting Library State - // ========================================================================= - /// Cached list of meetings - pub meetings: RwLock>, - - /// Currently selected meeting (for review mode) - pub selected_meeting: RwLock>, - - // ========================================================================= - // Trigger State - // ========================================================================= - /// Trigger service instance - pub trigger_service: Mutex>, - - /// Trigger detection enabled - pub trigger_enabled: RwLock, - - /// Trigger prompt pending (dialog shown) - pub trigger_pending: RwLock, - - /// Last trigger decision - pub trigger_decision: RwLock>, - - // ========================================================================= - // Summary State - // ========================================================================= - /// Current meeting summary - pub current_summary: RwLock>, - - /// Summary generation in progress - pub summary_loading: RwLock, - - /// Summary generation error - pub summary_error: RwLock>, - - // ========================================================================= - // Encryption State - // ========================================================================= - /// Crypto box for audio encryption - pub crypto: Arc, - - /// Meetings directory path - pub meetings_dir: RwLock, - - // ========================================================================= - // Configuration State - // ========================================================================= - /// User preferences (persisted) - pub preferences: RwLock>, -} - -impl AppState { - /// Create new application state with default preferences - pub fn new(crypto: Arc) -> Self { - Self::new_with_preferences(crypto, HashMap::new()) - } - - /// Create new application state with loaded preferences - pub fn new_with_preferences( - crypto: Arc, - prefs: HashMap, - ) -> Self { - // Get meetings directory from preferences or use default - let meetings_dir = prefs - .get("data_directory") - .and_then(|v| v.as_str()) - .map(|s| PathBuf::from(s).join("meetings")) - .unwrap_or_else(|| { - directories::ProjectDirs::from("com", "noteflow", "NoteFlow") - .map(|d| d.data_dir().join("meetings")) - .unwrap_or_else(|| { - directories::BaseDirs::new() - .map(|d| d.home_dir().join(".noteflow").join("meetings")) - .unwrap_or_else(|| PathBuf::from("/tmp/noteflow/meetings")) - }) - }); - - Self { - // Connection (GrpcClient is source of truth for connected/address) - grpc_client: GrpcClient::new(), - server_info: RwLock::new(None), - - // Recording - recording: RwLock::new(false), - current_meeting: RwLock::new(None), - recording_start_time: RwLock::new(None), - elapsed_seconds: RwLock::new(0), - - // Audio capture - current_db_level: RwLock::new(-60.0), - current_level_normalized: RwLock::new(0.0), - - // Transcript - transcript_segments: RwLock::new(Vec::new()), - current_partial_text: RwLock::new(String::new()), - - // Playback - audio_playback: RwLock::new(None), - playback_state: RwLock::new(PlaybackState::Stopped), - playback_position: RwLock::new(0.0), - playback_duration: RwLock::new(0.0), - playback_sample_rate: RwLock::new(DEFAULT_SAMPLE_RATE), - playback_samples_played: RwLock::new(0), - session_audio_buffer: RwLock::new(Vec::new()), - - // Sync - highlighted_segment_index: RwLock::new(None), - - // Annotations - annotations: RwLock::new(Vec::new()), - - // Meeting library - meetings: RwLock::new(Vec::new()), - selected_meeting: RwLock::new(None), - - // Triggers - trigger_service: Mutex::new(None), - trigger_enabled: RwLock::new(true), - trigger_pending: RwLock::new(false), - trigger_decision: RwLock::new(None), - - // Summary - current_summary: RwLock::new(None), - summary_loading: RwLock::new(false), - summary_error: RwLock::new(None), - - // Encryption - crypto, - meetings_dir: RwLock::new(meetings_dir), - - // Config - use loaded preferences - preferences: RwLock::new(prefs), - } - } - - /// Clear all transcript data - pub fn clear_transcript(&self) { - self.transcript_segments.write().clear(); - *self.current_partial_text.write() = String::new(); - } - - /// Reset all recording-related state - pub fn reset_recording_state(&self) { - *self.recording.write() = false; - *self.current_meeting.write() = None; - *self.recording_start_time.write() = None; - *self.elapsed_seconds.write() = 0; - } - - /// Clear session audio buffer and reset playback - pub fn clear_session_audio(&self) { - self.session_audio_buffer.write().clear(); - *self.playback_position.write() = 0.0; - *self.playback_duration.write() = 0.0; - *self.playback_sample_rate.write() = DEFAULT_SAMPLE_RATE; - *self.playback_samples_played.write() = 0; - *self.playback_state.write() = PlaybackState::Stopped; - } - - /// Find segment index containing the given playback position - /// - /// Uses binary search for O(log n) performance. - pub fn find_segment_at_position(&self, position: f64) -> Option { - let segments = self.transcript_segments.read(); - if segments.is_empty() { - return None; - } - - // Binary search for segment containing position - let mut left = 0; - let mut right = segments.len() - 1; - - while left <= right { - // Use overflow-safe midpoint calculation - let mid = left + (right - left) / 2; - let segment = &segments[mid]; - - if segment.start_time <= position && position <= segment.end_time { - return Some(mid); - } else if position < segment.start_time { - if mid == 0 { - break; - } - right = mid - 1; - } else { - left = mid + 1; - } - } - - None - } - - /// Get snapshot of current state for frontend - pub fn get_status(&self) -> AppStatus { - AppStatus { - // Connection state from gRPC client (single source of truth) - connected: self.grpc_client.is_connected(), - server_address: self.grpc_client.server_address(), - // Local state - recording: *self.recording.read(), - current_meeting_id: self.current_meeting.read().as_ref().map(|m| m.id.clone()), - elapsed_seconds: *self.elapsed_seconds.read(), - playback_state: *self.playback_state.read(), - playback_position: *self.playback_position.read(), - playback_duration: *self.playback_duration.read(), - segment_count: self.transcript_segments.read().len(), - annotation_count: self.annotations.read().len(), - trigger_enabled: *self.trigger_enabled.read(), - trigger_pending: *self.trigger_pending.read(), - summary_loading: *self.summary_loading.read(), - has_summary: self.current_summary.read().is_some(), - } - } -} - -/// Status snapshot for frontend -#[derive(Debug, Clone, Serialize)] -pub struct AppStatus { - pub connected: bool, - pub recording: bool, - pub server_address: String, - pub current_meeting_id: Option, - pub elapsed_seconds: u32, - pub playback_state: PlaybackState, - pub playback_position: f64, - pub playback_duration: f64, - pub segment_count: usize, - pub annotation_count: usize, - pub trigger_enabled: bool, - pub trigger_pending: bool, - pub summary_loading: bool, - pub has_summary: bool, -} - -/// Trigger status for frontend -#[derive(Debug, Clone, Serialize)] -pub struct TriggerStatus { - pub enabled: bool, - pub pending: bool, - pub decision: Option, - pub snoozed: bool, - pub snooze_remaining: f64, -} - -/// Playback info for frontend -#[derive(Debug, Clone, Serialize)] -pub struct PlaybackInfo { - pub state: PlaybackState, - pub position: f64, - pub duration: f64, - pub highlighted_segment: Option, -} diff --git a/client/src-tauri/src/state/mod.rs b/client/src-tauri/src/state/mod.rs deleted file mode 100644 index 86f9f9b..0000000 --- a/client/src-tauri/src/state/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Application state management -//! -//! This module defines the central `AppState` struct that holds all -//! application state and is shared across Tauri commands. - -mod app_state; - -pub use app_state::*; - -#[cfg(test)] -mod state_tests; diff --git a/client/src-tauri/src/state/state_tests.rs b/client/src-tauri/src/state/state_tests.rs deleted file mode 100644 index ce50a0a..0000000 --- a/client/src-tauri/src/state/state_tests.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Unit tests for application state management -//! -//! These tests verify state initialization, updates, and consistency. - -#[cfg(test)] -mod tests { - use crate::state::{ - PlaybackState, TriggerAction, TriggerDecision, TriggerSignal, TriggerSource, - }; - - #[test] - fn playback_state_equality() { - assert_eq!(PlaybackState::Stopped, PlaybackState::Stopped); - assert_eq!(PlaybackState::Playing, PlaybackState::Playing); - assert_eq!(PlaybackState::Paused, PlaybackState::Paused); - assert_ne!(PlaybackState::Stopped, PlaybackState::Playing); - } - - #[test] - fn playback_state_copy() { - let state = PlaybackState::Playing; - let copied = state; - assert_eq!(state, copied); - } - - #[test] - fn trigger_source_serialization() { - assert_eq!( - serde_json::to_string(&TriggerSource::AudioActivity).unwrap(), - "\"audio_activity\"" - ); - assert_eq!( - serde_json::to_string(&TriggerSource::ForegroundApp).unwrap(), - "\"foreground_app\"" - ); - assert_eq!( - serde_json::to_string(&TriggerSource::Calendar).unwrap(), - "\"calendar\"" - ); - } - - #[test] - fn trigger_action_serialization() { - assert_eq!( - serde_json::to_string(&TriggerAction::Ignore).unwrap(), - "\"ignore\"" - ); - assert_eq!( - serde_json::to_string(&TriggerAction::Notify).unwrap(), - "\"notify\"" - ); - assert_eq!( - serde_json::to_string(&TriggerAction::AutoStart).unwrap(), - "\"auto_start\"" - ); - } - - #[test] - fn trigger_signal_construction() { - let signal = TriggerSignal { - source: TriggerSource::ForegroundApp, - weight: 0.8, - app_name: Some("Zoom".to_string()), - timestamp: 1234567890.0, - }; - - assert_eq!(signal.source, TriggerSource::ForegroundApp); - assert!((signal.weight - 0.8).abs() < f32::EPSILON); - assert_eq!(signal.app_name, Some("Zoom".to_string())); - } - - #[test] - fn trigger_signal_without_app_name() { - let signal = TriggerSignal { - source: TriggerSource::AudioActivity, - weight: 0.6, - app_name: None, - timestamp: 1234567890.0, - }; - - assert_eq!(signal.app_name, None); - } - - #[test] - fn trigger_decision_construction() { - let decision = TriggerDecision { - action: TriggerAction::Notify, - confidence: 0.85, - signals: vec![TriggerSignal { - source: TriggerSource::ForegroundApp, - weight: 0.8, - app_name: Some("Zoom".to_string()), - timestamp: 1234567890.0, - }], - timestamp: 1234567890.0, - detected_app: Some("Zoom".to_string()), - }; - - assert_eq!(decision.action, TriggerAction::Notify); - assert!((decision.confidence - 0.85).abs() < f32::EPSILON); - assert_eq!(decision.signals.len(), 1); - assert_eq!(decision.detected_app, Some("Zoom".to_string())); - } - - #[test] - fn trigger_decision_auto_start() { - let decision = TriggerDecision { - action: TriggerAction::AutoStart, - confidence: 0.95, - signals: vec![], - timestamp: 1234567890.0, - detected_app: None, - }; - - assert_eq!(decision.action, TriggerAction::AutoStart); - assert!(decision.confidence > 0.9); - } - - #[test] - fn trigger_decision_serialization() { - let decision = TriggerDecision { - action: TriggerAction::Notify, - confidence: 0.75, - signals: vec![], - timestamp: 0.0, - detected_app: None, - }; - - let json = serde_json::to_string(&decision).unwrap(); - assert!(json.contains("\"action\":\"notify\"")); - assert!(json.contains("\"confidence\":0.75")); - } -} diff --git a/client/src-tauri/src/triggers/mod.rs b/client/src-tauri/src/triggers/mod.rs deleted file mode 100644 index 850551c..0000000 --- a/client/src-tauri/src/triggers/mod.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Trigger detection for auto-starting recordings -//! -//! This module provides detection of meeting-related events: -//! - Foreground app detection (Zoom, Teams, etc.) -//! - Audio activity detection -//! - Calendar event proximity - -use std::time::Instant; - -/// Trigger service for detecting meeting starts -pub struct TriggerService { - snooze_until: Option, - last_check: Option, -} - -impl Default for TriggerService { - fn default() -> Self { - Self::new() - } -} - -impl TriggerService { - /// Create a new trigger service - pub fn new() -> Self { - Self { - snooze_until: None, - last_check: None, - } - } - - /// Snooze triggers for a duration - pub fn snooze(&mut self, seconds: Option) { - const DEFAULT_SNOOZE: f64 = 300.0; // 5 minutes - const MAX_SNOOZE: f64 = 24.0 * 60.0 * 60.0; // 24 hours - - let mut duration = seconds.unwrap_or(DEFAULT_SNOOZE); - - // Validate duration to prevent panics from invalid f64 values - if !duration.is_finite() || duration <= 0.0 { - duration = DEFAULT_SNOOZE; - } else if duration > MAX_SNOOZE { - duration = MAX_SNOOZE; - } - - self.snooze_until = Some(Instant::now() + std::time::Duration::from_secs_f64(duration)); - } - - /// Reset snooze - pub fn reset_snooze(&mut self) { - self.snooze_until = None; - } - - /// Check if currently snoozed - pub fn is_snoozed(&self) -> bool { - self.snooze_until - .map(|t| Instant::now() < t) - .unwrap_or(false) - } - - /// Get remaining snooze seconds - pub fn snooze_remaining_seconds(&self) -> f64 { - self.snooze_until - .map(|t| { - let now = Instant::now(); - if now < t { - (t - now).as_secs_f64() - } else { - 0.0 - } - }) - .unwrap_or(0.0) - } - - /// Check for triggers - pub fn check(&mut self) -> Option { - if self.is_snoozed() { - return None; - } - - self.last_check = Some(Instant::now()); - - // TODO: Implement actual trigger detection - // - Check foreground app (active-win-pos-rs) - // - Check audio activity - // - Check calendar events - - None - } -} - -/// Get the currently focused application name -#[allow(dead_code)] -pub fn get_foreground_app() -> Option { - // TODO: Use active-win-pos-rs to get foreground app - None -} - -/// Check if the given app name is a known meeting app -#[allow(dead_code)] -pub fn is_meeting_app(app_name: &str) -> bool { - let meeting_apps = [ - "zoom", - "teams", - "slack", - "meet", - "webex", - "skype", - "discord", - "gotomeeting", - ]; - - let app_lower = app_name.to_lowercase(); - meeting_apps.iter().any(|&app| app_lower.contains(app)) -} diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json deleted file mode 100644 index dd84ed9..0000000 --- a/client/src-tauri/tauri.conf.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2.0.0", - "productName": "NoteFlow", - "version": "0.1.0", - "identifier": "com.noteflow.app", - "build": { - "beforeDevCommand": "npm run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm run build", - "frontendDist": "../dist" - }, - "app": { - "windows": [ - { - "title": "NoteFlow", - "width": 1024, - "height": 768, - "minWidth": 800, - "minHeight": 600, - "resizable": true, - "fullscreen": false, - "center": true - } - ], - "security": { - "csp": "default-src 'self'; connect-src 'self' http://localhost:* https://localhost:*" - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "linux": { - "appimage": { - "bundleMediaFramework": true - }, - "deb": { - "depends": ["libportaudio2", "libasound2"] - } - }, - "macOS": { - "minimumSystemVersion": "10.15" - }, - "windows": { - "webviewInstallMode": { - "type": "downloadBootstrapper" - } - } - }, - "plugins": { - "dialog": {}, - "fs": { - "scope": ["$APPDATA/**", "$HOME/.noteflow/**"] - }, - "notification": {}, - "shell": { - "open": true - }, - "global-shortcut": {}, - "window-state": {} - } -} diff --git a/client/src-tauri/tests/audio_unit_tests.rs b/client/src-tauri/tests/audio_unit_tests.rs deleted file mode 100644 index 1b2effb..0000000 --- a/client/src-tauri/tests/audio_unit_tests.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! Unit tests for audio subsystem edge cases. -//! -//! Run with: cargo test --test audio_unit_tests - -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - -// ============================================================================= -// Device ID Stability Tests -// ============================================================================= - -/// Replicate the stable_device_id function for testing -fn stable_device_id(name: &str) -> u32 { - let mut hasher = DefaultHasher::new(); - name.hash(&mut hasher); - (hasher.finish() % u32::MAX as u64) as u32 -} - -#[test] -fn test_stable_device_id_same_for_same_name() { - let id1 = stable_device_id("USB Audio Device"); - let id2 = stable_device_id("USB Audio Device"); - - assert_eq!(id1, id2, "Same name should produce same ID"); -} - -#[test] -fn test_stable_device_id_different_for_different_names() { - let id1 = stable_device_id("USB Audio Device"); - let id2 = stable_device_id("Built-in Microphone"); - - assert_ne!(id1, id2, "Different names should produce different IDs"); -} - -#[test] -fn test_stable_device_id_handles_special_characters() { - // Device names can contain special chars - let id1 = stable_device_id("USB (2.0) Device: Main"); - let id2 = stable_device_id("USB (2.0) Device: Main"); - - assert_eq!(id1, id2); -} - -#[test] -fn test_stable_device_id_handles_unicode() { - let id1 = stable_device_id("マイク デバイス"); - let id2 = stable_device_id("マイク デバイス"); - - assert_eq!(id1, id2); -} - -#[test] -fn test_stable_device_id_handles_empty_string() { - let id = stable_device_id(""); - // Should not panic, should return some consistent value - assert_eq!(id, stable_device_id("")); -} - -#[test] -fn test_stable_device_id_consistent_across_calls() { - let name = "Microphone Array (Realtek)"; - let ids: Vec = (0..100).map(|_| stable_device_id(name)).collect(); - - // All IDs should be identical - assert!(ids.iter().all(|&id| id == ids[0])); -} - -// ============================================================================= -// Audio Loader Edge Cases -// ============================================================================= - -#[test] -fn test_samples_to_chunks_basic() { - // Replicate the function logic - fn samples_to_chunks(samples: &[f32], sample_rate: u32) -> Vec<(f64, f64, usize)> { - let chunk_duration = 0.1; - let chunk_samples = ((sample_rate as f64 * chunk_duration) as usize).max(1); - let mut chunks = Vec::new(); - let mut offset = 0; - - while offset < samples.len() { - let end = (offset + chunk_samples).min(samples.len()); - let frame_count = end - offset; - let duration = frame_count as f64 / sample_rate as f64; - let timestamp = offset as f64 / sample_rate as f64; - chunks.push((timestamp, duration, frame_count)); - offset = end; - } - chunks - } - - let samples: Vec = vec![0.0; 48000]; // 1 second at 48kHz - let chunks = samples_to_chunks(&samples, 48000); - - // Should have ~10 chunks of 0.1s each - assert_eq!(chunks.len(), 10); - assert!((chunks[0].1 - 0.1).abs() < 0.001); -} - -#[test] -fn test_samples_to_chunks_odd_sample_count() { - fn samples_to_chunks(samples: &[f32], sample_rate: u32) -> Vec<(f64, f64, usize)> { - let chunk_duration = 0.1; - let chunk_samples = ((sample_rate as f64 * chunk_duration) as usize).max(1); - let mut chunks = Vec::new(); - let mut offset = 0; - - while offset < samples.len() { - let end = (offset + chunk_samples).min(samples.len()); - let frame_count = end - offset; - let duration = frame_count as f64 / sample_rate as f64; - let timestamp = offset as f64 / sample_rate as f64; - chunks.push((timestamp, duration, frame_count)); - offset = end; - } - chunks - } - - // Odd number of samples that doesn't divide evenly - let samples: Vec = vec![0.0; 4801]; - let chunks = samples_to_chunks(&samples, 48000); - - // Last chunk should be smaller - let last = chunks.last().unwrap(); - assert!(last.2 < 4800); // Less than a full chunk -} - -#[test] -fn test_samples_to_chunks_low_sample_rate() { - fn samples_to_chunks(samples: &[f32], sample_rate: u32) -> Vec<(f64, f64, usize)> { - let chunk_duration = 0.1; - let chunk_samples = ((sample_rate as f64 * chunk_duration) as usize).max(1); - let mut chunks = Vec::new(); - let mut offset = 0; - - while offset < samples.len() { - let end = (offset + chunk_samples).min(samples.len()); - let frame_count = end - offset; - let duration = frame_count as f64 / sample_rate as f64; - let timestamp = offset as f64 / sample_rate as f64; - chunks.push((timestamp, duration, frame_count)); - offset = end; - } - chunks - } - - // Very low sample rate - let samples: Vec = vec![0.0; 100]; - let chunks = samples_to_chunks(&samples, 100); // 100 Hz - - // At 100Hz, 0.1s = 10 samples per chunk, so 100 samples = 10 chunks - assert_eq!(chunks.len(), 10); -} - -#[test] -fn test_samples_to_chunks_minimum_chunk_size() { - fn samples_to_chunks(samples: &[f32], sample_rate: u32) -> Vec<(f64, f64, usize)> { - let chunk_duration = 0.1; - let chunk_samples = ((sample_rate as f64 * chunk_duration) as usize).max(1); - let mut chunks = Vec::new(); - let mut offset = 0; - - while offset < samples.len() { - let end = (offset + chunk_samples).min(samples.len()); - let frame_count = end - offset; - let duration = frame_count as f64 / sample_rate as f64; - let timestamp = offset as f64 / sample_rate as f64; - chunks.push((timestamp, duration, frame_count)); - offset = end; - } - chunks - } - - // Sample rate so low that 0.1s < 1 sample - should clamp to 1 - let samples: Vec = vec![0.0; 5]; - let chunks = samples_to_chunks(&samples, 5); // 5 Hz → 0.5 samples/chunk → clamped to 1 - - // With .max(1), each chunk has at least 1 sample - assert_eq!(chunks.len(), 5); - for chunk in &chunks { - assert!(chunk.2 >= 1, "Chunk should have at least 1 sample"); - } -} - -#[test] -fn test_samples_to_chunks_empty_input() { - fn samples_to_chunks(samples: &[f32], sample_rate: u32) -> Vec<(f64, f64, usize)> { - let chunk_duration = 0.1; - let chunk_samples = ((sample_rate as f64 * chunk_duration) as usize).max(1); - let mut chunks = Vec::new(); - let mut offset = 0; - - while offset < samples.len() { - let end = (offset + chunk_samples).min(samples.len()); - let frame_count = end - offset; - let duration = frame_count as f64 / sample_rate as f64; - let timestamp = offset as f64 / sample_rate as f64; - chunks.push((timestamp, duration, frame_count)); - offset = end; - } - chunks - } - - let samples: Vec = vec![]; - let chunks = samples_to_chunks(&samples, 48000); - - assert!(chunks.is_empty()); -} - -// ============================================================================= -// Sample Rate Validation Tests -// ============================================================================= - -#[test] -fn test_sample_rate_zero_rejected() { - // This tests the validation logic: sample_rate == 0 should be rejected - fn validate_sample_rate(sample_rate: u32) -> Result<(), &'static str> { - if sample_rate == 0 { - return Err("Invalid sample rate: 0"); - } - Ok(()) - } - - assert!(validate_sample_rate(0).is_err()); - assert!(validate_sample_rate(16000).is_ok()); - assert!(validate_sample_rate(48000).is_ok()); -} - -#[test] -fn test_duration_calculation_various_rates() { - fn calculate_duration(num_samples: usize, sample_rate: u32) -> f64 { - if sample_rate == 0 { - return 0.0; - } - num_samples as f64 / sample_rate as f64 - } - - // 16kHz: 16000 samples = 1 second - assert!((calculate_duration(16000, 16000) - 1.0).abs() < 0.001); - - // 48kHz: 48000 samples = 1 second - assert!((calculate_duration(48000, 48000) - 1.0).abs() < 0.001); - - // 44.1kHz: 44100 samples = 1 second - assert!((calculate_duration(44100, 44100) - 1.0).abs() < 0.001); - - // Edge case: 0 sample rate - assert_eq!(calculate_duration(1000, 0), 0.0); -} - -// ============================================================================= -// Playback Position Edge Cases -// ============================================================================= - -#[test] -fn test_seek_position_clamping() { - fn clamp_position(pos: f64, duration: f64) -> f64 { - pos.clamp(0.0, duration.max(0.0)) - } - - // Normal case - assert!((clamp_position(50.0, 100.0) - 50.0).abs() < 0.001); - - // Beyond duration - assert!((clamp_position(150.0, 100.0) - 100.0).abs() < 0.001); - - // Negative - assert!((clamp_position(-10.0, 100.0) - 0.0).abs() < 0.001); - - // Zero duration - assert!((clamp_position(50.0, 0.0) - 0.0).abs() < 0.001); - - // Negative duration (edge case) - assert!((clamp_position(50.0, -10.0) - 0.0).abs() < 0.001); -} - -#[test] -fn test_position_tracking_accumulation() { - // Simulate position tracker accumulating samples - let sample_rate: u32 = 48000; - let samples_per_update: u64 = 4800; // 0.1 second chunks - - let mut samples_played: u64 = 0; - - // Simulate 10 updates - for _ in 0..10 { - samples_played += samples_per_update; - } - - let position = samples_played as f64 / sample_rate as f64; - assert!((position - 1.0).abs() < 0.001, "Should be ~1 second"); -} - -#[test] -fn test_position_tracking_pause_resume() { - // Simulate pause/resume preserving position - let sample_rate: u32 = 48000; - let mut samples_played: u64 = 0; - - // Play for 0.5 seconds - samples_played += 24000; - - // Pause - samples_played is preserved - let paused_samples = samples_played; - - // Resume - should continue from paused position - samples_played = paused_samples; - samples_played += 24000; - - let position = samples_played as f64 / sample_rate as f64; - assert!((position - 1.0).abs() < 0.001, "Should be 1 second total"); -} - -// ============================================================================= -// Segment Finding Edge Cases -// ============================================================================= - -#[derive(Debug, Clone)] -struct MockSegment { - id: i32, - start_time: f64, - end_time: f64, -} - -fn find_segment_at_position(segments: &[MockSegment], position: f64) -> Option { - for seg in segments { - if position >= seg.start_time && position < seg.end_time { - return Some(seg.id); - } - } - None -} - -#[test] -fn test_find_segment_in_gap() { - let segments = vec![ - MockSegment { id: 1, start_time: 0.0, end_time: 5.0 }, - MockSegment { id: 2, start_time: 10.0, end_time: 15.0 }, - ]; - - // Position in gap between segments - let result = find_segment_at_position(&segments, 7.5); - assert!(result.is_none(), "Should return None for gap"); -} - -#[test] -fn test_find_segment_at_boundary() { - let segments = vec![ - MockSegment { id: 1, start_time: 0.0, end_time: 5.0 }, - MockSegment { id: 2, start_time: 5.0, end_time: 10.0 }, - ]; - - // Exactly at segment boundary - let result = find_segment_at_position(&segments, 5.0); - assert_eq!(result, Some(2), "Should be in second segment"); -} - -#[test] -fn test_find_segment_before_all() { - let segments = vec![ - MockSegment { id: 1, start_time: 5.0, end_time: 10.0 }, - ]; - - let result = find_segment_at_position(&segments, 2.0); - assert!(result.is_none(), "Should return None before first segment"); -} - -#[test] -fn test_find_segment_after_all() { - let segments = vec![ - MockSegment { id: 1, start_time: 0.0, end_time: 5.0 }, - ]; - - let result = find_segment_at_position(&segments, 10.0); - assert!(result.is_none(), "Should return None after last segment"); -} - -#[test] -fn test_find_segment_empty_list() { - let segments: Vec = vec![]; - let result = find_segment_at_position(&segments, 5.0); - assert!(result.is_none()); -} diff --git a/client/src-tauri/tests/grpc_integration.rs b/client/src-tauri/tests/grpc_integration.rs deleted file mode 100644 index 9366fe5..0000000 --- a/client/src-tauri/tests/grpc_integration.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! Integration tests for gRPC client against live server. -//! -//! These tests require the NoteFlow server running at localhost:50051. -//! Run with: cargo test --test grpc_integration -- --test-threads=1 - -use std::time::Duration; -use tokio::time::timeout; - -// Import the library -use noteflow_tauri_lib::grpc::{GrpcClient, AnnotationType}; - -const SERVER_ADDR: &str = "localhost:50051"; -const TEST_TIMEOUT: Duration = Duration::from_secs(10); - -/// Helper to create a connected client or skip test if server unavailable -async fn connected_client() -> Option { - let client = GrpcClient::new(); - match timeout(Duration::from_secs(2), client.connect(SERVER_ADDR)).await { - Ok(Ok(_)) => Some(client), - Ok(Err(e)) => { - eprintln!("Server unavailable: {e} - skipping integration test"); - None - } - Err(_) => { - eprintln!("Connection timeout - skipping integration test"); - None - } - } -} - -// ============================================================================= -// Connection Tests -// ============================================================================= - -#[tokio::test] -async fn test_connect_to_server() { - let client = GrpcClient::new(); - - let result = timeout(TEST_TIMEOUT, client.connect(SERVER_ADDR)).await; - - match result { - Ok(Ok(info)) => { - assert!(client.is_connected()); - assert!(!info.version.is_empty(), "Server should return version"); - } - Ok(Err(e)) => { - eprintln!("Server not available: {e}"); - } - Err(_) => { - panic!("Connection timed out"); - } - } -} - -#[tokio::test] -async fn test_disconnect_clears_state() { - let Some(client) = connected_client().await else { return }; - - assert!(client.is_connected()); - - client.disconnect().await; - - assert!(!client.is_connected()); - assert!(client.server_address().is_empty()); -} - -#[tokio::test] -async fn test_operations_fail_when_disconnected() { - let client = GrpcClient::new(); - - // Should fail without connection - let result = client.list_annotations("meeting-123", 0.0, 100.0).await; - assert!(result.is_err()); - - let err = result.unwrap_err(); - assert!(err.to_string().contains("Not connected")); -} - -// ============================================================================= -// Annotation CRUD Tests -// ============================================================================= - -#[tokio::test] -async fn test_annotation_add_and_get() { - let Some(client) = connected_client().await else { return }; - - // First create a meeting to attach annotation to - let meeting = client.create_meeting("Test Meeting for Annotations", None).await; - let Ok(meeting) = meeting else { - eprintln!("Could not create meeting - skipping"); - return; - }; - - // Add annotation - let result = client.add_annotation( - &meeting.id, - AnnotationType::ActionItem, - "Follow up with team", - 10.0, - 15.0, - Some(vec![1, 2]), - ).await; - - match result { - Ok(annotation) => { - assert!(!annotation.id.is_empty()); - assert_eq!(annotation.meeting_id, meeting.id); - assert_eq!(annotation.text, "Follow up with team"); - assert!((annotation.start_time - 10.0).abs() < 0.001); - assert!((annotation.end_time - 15.0).abs() < 0.001); - - // Get the annotation back - let fetched = client.get_annotation(&annotation.id).await; - match fetched { - Ok(a) => { - assert_eq!(a.id, annotation.id); - assert_eq!(a.text, annotation.text); - } - Err(e) => eprintln!("Get annotation failed: {e}"), - } - } - Err(e) => { - eprintln!("Add annotation failed (server may not support): {e}"); - } - } - - // Cleanup - let _ = client.delete_meeting(&meeting.id).await; -} - -#[tokio::test] -async fn test_annotation_list_filters_by_time() { - let Some(client) = connected_client().await else { return }; - - // Create meeting - let meeting = client.create_meeting("List Filter Test", None).await; - let Ok(meeting) = meeting else { return }; - - // Add annotations at different times - let _ = client.add_annotation(&meeting.id, AnnotationType::Note, "Early note", 5.0, 10.0, None).await; - let _ = client.add_annotation(&meeting.id, AnnotationType::Note, "Middle note", 50.0, 55.0, None).await; - let _ = client.add_annotation(&meeting.id, AnnotationType::Note, "Late note", 100.0, 105.0, None).await; - - // List only middle range - let result = client.list_annotations(&meeting.id, 40.0, 60.0).await; - - match result { - Ok(annotations) => { - // Should only include annotations overlapping 40-60s range - for ann in &annotations { - assert!( - ann.end_time >= 40.0 && ann.start_time <= 60.0, - "Annotation outside time range: {}-{}", - ann.start_time, - ann.end_time - ); - } - } - Err(e) => eprintln!("List annotations failed: {e}"), - } - - // Cleanup - let _ = client.delete_meeting(&meeting.id).await; -} - -#[tokio::test] -async fn test_annotation_update() { - let Some(client) = connected_client().await else { return }; - - let meeting = client.create_meeting("Update Test", None).await; - let Ok(meeting) = meeting else { return }; - - // Add annotation - let added = client.add_annotation( - &meeting.id, - AnnotationType::Note, - "Original text", - 0.0, - 10.0, - None, - ).await; - - let Ok(annotation) = added else { return }; - - // Update it - let updated = client.update_annotation( - &annotation.id, - Some(AnnotationType::Decision), - Some("Updated text"), - None, - None, - None, - ).await; - - match updated { - Ok(a) => { - assert_eq!(a.text, "Updated text"); - // Type should be updated - assert_eq!(a.annotation_type, AnnotationType::Decision); - } - Err(e) => eprintln!("Update annotation failed: {e}"), - } - - // Cleanup - let _ = client.delete_meeting(&meeting.id).await; -} - -#[tokio::test] -async fn test_annotation_delete() { - let Some(client) = connected_client().await else { return }; - - let meeting = client.create_meeting("Delete Test", None).await; - let Ok(meeting) = meeting else { return }; - - // Add annotation - let added = client.add_annotation( - &meeting.id, - AnnotationType::Risk, - "To be deleted", - 0.0, - 5.0, - None, - ).await; - - let Ok(annotation) = added else { return }; - - // Delete it - let result = client.delete_annotation(&annotation.id).await; - - match result { - Ok(success) => { - assert!(success); - - // Verify it's gone - let fetch = client.get_annotation(&annotation.id).await; - assert!(fetch.is_err(), "Deleted annotation should not be fetchable"); - } - Err(e) => eprintln!("Delete annotation failed: {e}"), - } - - // Cleanup - let _ = client.delete_meeting(&meeting.id).await; -} - -// ============================================================================= -// Edge Case Tests -// ============================================================================= - -#[tokio::test] -async fn test_annotation_with_empty_text() { - let Some(client) = connected_client().await else { return }; - - let meeting = client.create_meeting("Empty Text Test", None).await; - let Ok(meeting) = meeting else { return }; - - // Empty text should be allowed or rejected gracefully - let result = client.add_annotation( - &meeting.id, - AnnotationType::Note, - "", // Empty text - 0.0, - 5.0, - None, - ).await; - - // Either succeeds with empty or returns proper error - shouldn't panic - match result { - Ok(ann) => assert!(ann.text.is_empty()), - Err(e) => assert!(!e.to_string().is_empty()), - } - - let _ = client.delete_meeting(&meeting.id).await; -} - -#[tokio::test] -async fn test_annotation_with_negative_times() { - let Some(client) = connected_client().await else { return }; - - let meeting = client.create_meeting("Negative Time Test", None).await; - let Ok(meeting) = meeting else { return }; - - // Negative times - should be rejected or handled gracefully - let result = client.add_annotation( - &meeting.id, - AnnotationType::Note, - "Negative time test", - -5.0, // Negative start - -2.0, // Negative end - None, - ).await; - - // Should either reject or clamp - shouldn't panic - match result { - Ok(_) => { /* Server accepted it - valid behavior */ } - Err(e) => assert!(!e.to_string().is_empty()), - } - - let _ = client.delete_meeting(&meeting.id).await; -} - -#[tokio::test] -async fn test_annotation_on_nonexistent_meeting() { - let Some(client) = connected_client().await else { return }; - - let result = client.add_annotation( - "nonexistent-meeting-id-12345", - AnnotationType::Note, - "Should fail", - 0.0, - 5.0, - None, - ).await; - - // Should return error, not panic - assert!(result.is_err(), "Should fail for nonexistent meeting"); -} - -#[tokio::test] -async fn test_get_nonexistent_annotation() { - let Some(client) = connected_client().await else { return }; - - let result = client.get_annotation("nonexistent-annotation-id-12345").await; - - // Should return error, not panic - assert!(result.is_err(), "Should fail for nonexistent annotation"); -} diff --git a/client/src-tauri/tests/preferences_tests.rs b/client/src-tauri/tests/preferences_tests.rs deleted file mode 100644 index 2aac4b0..0000000 --- a/client/src-tauri/tests/preferences_tests.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! Tests for preferences persistence and security. -//! -//! Run with: cargo test --test preferences_tests - -use std::collections::HashMap; -use std::path::PathBuf; -use tempfile::TempDir; - -// ============================================================================= -// Preferences File Persistence Tests -// ============================================================================= - -#[test] -fn test_preferences_round_trip() { - let dir = TempDir::new().unwrap(); - let path = dir.path().join("preferences.json"); - - let mut prefs: HashMap = HashMap::new(); - prefs.insert("server_url".to_string(), serde_json::json!("localhost:50051")); - prefs.insert("data_directory".to_string(), serde_json::json!("/data/noteflow")); - prefs.insert("auto_connect".to_string(), serde_json::json!(true)); - - // Write - let json = serde_json::to_string_pretty(&prefs).unwrap(); - std::fs::write(&path, &json).unwrap(); - - // Read back - let read_json = std::fs::read_to_string(&path).unwrap(); - let loaded: HashMap = serde_json::from_str(&read_json).unwrap(); - - assert_eq!(loaded.get("server_url"), prefs.get("server_url")); - assert_eq!(loaded.get("data_directory"), prefs.get("data_directory")); - assert_eq!(loaded.get("auto_connect"), prefs.get("auto_connect")); -} - -#[test] -fn test_preferences_empty_file() { - let dir = TempDir::new().unwrap(); - let path = dir.path().join("preferences.json"); - - std::fs::write(&path, "{}").unwrap(); - - let json = std::fs::read_to_string(&path).unwrap(); - let loaded: HashMap = serde_json::from_str(&json).unwrap(); - - assert!(loaded.is_empty()); -} - -#[test] -fn test_preferences_missing_file() { - let dir = TempDir::new().unwrap(); - let path = dir.path().join("nonexistent.json"); - - // Should handle gracefully - let result = std::fs::read_to_string(&path); - assert!(result.is_err()); - - // Default behavior: return empty HashMap - let prefs: HashMap = if path.exists() { - serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap_or_default() - } else { - HashMap::new() - }; - - assert!(prefs.is_empty()); -} - -#[test] -fn test_preferences_corrupt_file() { - let dir = TempDir::new().unwrap(); - let path = dir.path().join("corrupt.json"); - - // Write invalid JSON - std::fs::write(&path, "{ invalid json ").unwrap(); - - let json = std::fs::read_to_string(&path).unwrap(); - let result: Result, _> = serde_json::from_str(&json); - - assert!(result.is_err(), "Should fail to parse corrupt JSON"); - - // Fallback should be empty - let prefs = result.unwrap_or_default(); - assert!(prefs.is_empty()); -} - -#[test] -fn test_preferences_creates_parent_directory() { - let dir = TempDir::new().unwrap(); - let nested_path = dir.path().join("nested").join("deep").join("preferences.json"); - - // Parent doesn't exist yet - assert!(!nested_path.parent().unwrap().exists()); - - // Create parent and write - if let Some(parent) = nested_path.parent() { - std::fs::create_dir_all(parent).unwrap(); - } - std::fs::write(&nested_path, "{}").unwrap(); - - assert!(nested_path.exists()); -} - -// ============================================================================= -// API Key Masking Tests -// ============================================================================= - -fn mask_api_key(key: &str) -> String { - if key.len() <= 8 { - "*".repeat(key.len()) - } else { - format!("{}...{}", &key[..4], &key[key.len()-4..]) - } -} - -#[test] -fn test_api_key_masking_long_key() { - let key = "sk-1234567890abcdef"; - let masked = mask_api_key(key); - - assert_eq!(masked, "sk-1...cdef"); - assert!(!masked.contains("567890")); -} - -#[test] -fn test_api_key_masking_short_key() { - let key = "abc123"; - let masked = mask_api_key(key); - - assert_eq!(masked, "******"); -} - -#[test] -fn test_api_key_masking_empty() { - let key = ""; - let masked = mask_api_key(key); - - assert_eq!(masked, ""); -} - -#[test] -fn test_api_key_masking_boundary() { - // Exactly 8 chars - let key = "12345678"; - let masked = mask_api_key(key); - - assert_eq!(masked, "********"); -} - -#[test] -fn test_api_key_masking_9_chars() { - // 9 chars - should show first 4 and last 4 - let key = "123456789"; - let masked = mask_api_key(key); - - assert_eq!(masked, "1234...6789"); -} - -fn is_masked_key(key: &str) -> bool { - key.contains("...") || key.chars().all(|c| c == '*') -} - -#[test] -fn test_detect_masked_key() { - assert!(is_masked_key("sk-1...cdef")); - assert!(is_masked_key("******")); - assert!(is_masked_key("****")); - assert!(!is_masked_key("sk-1234567890")); - assert!(!is_masked_key("real-api-key")); -} - -// ============================================================================= -// Preferences Update Logic Tests -// ============================================================================= - -#[test] -fn test_preferences_merge_update() { - let mut existing: HashMap = HashMap::new(); - existing.insert("server_url".to_string(), serde_json::json!("old.server:50051")); - existing.insert("keep_this".to_string(), serde_json::json!("preserved")); - - // New preferences to merge - let updates: HashMap = [ - ("server_url".to_string(), serde_json::json!("new.server:50051")), - ("new_key".to_string(), serde_json::json!("added")), - ].into_iter().collect(); - - // Merge - for (key, value) in updates { - existing.insert(key, value); - } - - assert_eq!(existing.get("server_url"), Some(&serde_json::json!("new.server:50051"))); - assert_eq!(existing.get("keep_this"), Some(&serde_json::json!("preserved"))); - assert_eq!(existing.get("new_key"), Some(&serde_json::json!("added"))); -} - -#[test] -fn test_preferences_exclude_api_key_from_file() { - let mut prefs: HashMap = HashMap::new(); - prefs.insert("server_url".to_string(), serde_json::json!("localhost:50051")); - prefs.insert("cloud_api_key".to_string(), serde_json::json!("secret-key")); - - // Before persisting, remove API key - prefs.remove("cloud_api_key"); - - let json = serde_json::to_string(&prefs).unwrap(); - - assert!(!json.contains("secret-key")); - assert!(!json.contains("cloud_api_key")); - assert!(json.contains("server_url")); -} - -// ============================================================================= -// Config Directory Tests -// ============================================================================= - -#[test] -fn test_config_directory_resolution() { - // Test the fallback logic for config directory - fn get_config_dir() -> PathBuf { - directories::ProjectDirs::from("com", "noteflow", "NoteFlow") - .map(|d| d.config_dir().to_path_buf()) - .unwrap_or_else(|| PathBuf::from("/tmp/noteflow")) - } - - let dir = get_config_dir(); - - // Should be a valid path (not empty) - assert!(!dir.as_os_str().is_empty()); - - // Should contain "noteflow" somewhere in the path - let path_str = dir.to_string_lossy().to_lowercase(); - assert!(path_str.contains("noteflow"), "Config dir should contain 'noteflow': {}", path_str); -} - -// ============================================================================= -// Selected Device Persistence Tests -// ============================================================================= - -#[test] -fn test_device_selection_persistence_format() { - // Device selection should store both ID and name for robustness - #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] - struct DeviceSelection { - id: u32, - name: String, - } - - let selection = DeviceSelection { - id: 123456789, - name: "USB Microphone".to_string(), - }; - - let json = serde_json::to_string(&selection).unwrap(); - let loaded: DeviceSelection = serde_json::from_str(&json).unwrap(); - - assert_eq!(loaded, selection); -} - -#[test] -fn test_device_selection_lookup_by_name() { - // When device IDs change, lookup by name should work - #[derive(Debug)] - struct Device { - id: u32, - name: String, - } - - let devices = vec![ - Device { id: 111, name: "Built-in Mic".to_string() }, - Device { id: 222, name: "USB Microphone".to_string() }, - ]; - - // Stored preference has old ID but valid name - let stored_name = "USB Microphone"; - - let found = devices.iter().find(|d| d.name == stored_name); - assert!(found.is_some()); - assert_eq!(found.unwrap().id, 222); -} diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index 1ca8a09..0000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useEffect } from 'react'; -import { ConnectionPanel } from '@/components/connection/ConnectionPanel'; -import { MeetingLibrary } from '@/components/meetings/MeetingLibrary'; -import { RecordingPanel } from '@/components/recording/RecordingPanel'; -import { SettingsPanel } from '@/components/settings'; -import { SummaryPanel } from '@/components/summary/SummaryPanel'; -import { TranscriptView } from '@/components/transcript/TranscriptView'; -import { TriggerDialog } from '@/components/triggers/TriggerDialog'; -import { useTauriEvents } from '@/hooks/useTauriEvents'; -import { api } from '@/lib/tauri'; -import { useStore } from '@/store'; - -export function App() { - const { viewMode, setViewMode, serverAddress, setConnectionState } = useStore(); - - // Set up Tauri event listeners - useTauriEvents(); - - // Connect to server on mount - useEffect(() => { - let isMounted = true; - let attemptId = 0; - - const connectToServer = async () => { - const myAttempt = ++attemptId; - - try { - const info = await api.connection.connect(serverAddress); - if (isMounted && myAttempt === attemptId) { - setConnectionState(true, serverAddress, info); - } - } catch (error) { - console.error('Failed to connect:', error); - if (isMounted && myAttempt === attemptId) { - setConnectionState(false, serverAddress); - } - } - }; - - connectToServer(); - - return () => { - isMounted = false; - attemptId++; - }; - }, [serverAddress, setConnectionState]); - - return ( -
- {/* Header */} -
-
-

NoteFlow

- -
- -
- - {/* Main Content */} -
- {viewMode === 'recording' && } - {viewMode === 'library' && } - {viewMode === 'settings' && } -
- - {/* Trigger Dialog (modal) */} - -
- ); -} - -function NavButton({ - view, - currentView, - onClick, - children, -}: { - view: string; - currentView: string; - onClick: () => void; - children: React.ReactNode; -}) { - const isActive = view === currentView; - - return ( - - ); -} - -function RecordingView() { - return ( -
- {/* Left panel: Controls and transcript */} -
- - -
- - {/* Right panel: Summary */} -
- -
-
- ); -} - diff --git a/client/src/components/annotations/AnnotationBadge.tsx b/client/src/components/annotations/AnnotationBadge.tsx deleted file mode 100644 index 990dadc..0000000 --- a/client/src/components/annotations/AnnotationBadge.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { AlertCircle, AlertTriangle, CheckSquare, StickyNote, X } from 'lucide-react'; -import type { AnnotationType } from '@/types/annotation'; - -interface AnnotationBadgeProps { - type: AnnotationType; - text: string; - onRemove?: () => void; -} - -const ANNOTATION_CONFIG: Record< - AnnotationType, - { icon: typeof CheckSquare; bgColor: string; textColor: string; label: string } -> = { - action_item: { - icon: CheckSquare, - bgColor: 'bg-green-900/30', - textColor: 'text-green-400', - label: 'Action', - }, - decision: { - icon: AlertCircle, - bgColor: 'bg-purple-900/30', - textColor: 'text-purple-400', - label: 'Decision', - }, - note: { - icon: StickyNote, - bgColor: 'bg-blue-900/30', - textColor: 'text-blue-400', - label: 'Note', - }, - risk: { - icon: AlertTriangle, - bgColor: 'bg-red-900/30', - textColor: 'text-red-400', - label: 'Risk', - }, -}; - -export function AnnotationBadge({ type, text, onRemove }: AnnotationBadgeProps) { - const config = ANNOTATION_CONFIG[type]; - const Icon = config.icon; - - return ( -
- - {text || config.label} - {onRemove && ( - - )} -
- ); -} diff --git a/client/src/components/annotations/AnnotationList.tsx b/client/src/components/annotations/AnnotationList.tsx deleted file mode 100644 index e2ff9d3..0000000 --- a/client/src/components/annotations/AnnotationList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { AnnotationInfo } from '@/types/annotation'; -import { AnnotationBadge } from './AnnotationBadge'; - -interface AnnotationListProps { - annotations: AnnotationInfo[]; - onRemove?: (annotationId: string) => void; -} - -export function AnnotationList({ annotations, onRemove }: AnnotationListProps) { - if (annotations.length === 0) { - return null; - } - - return ( -
- {annotations.map((annotation) => ( - onRemove(annotation.id) : undefined} - /> - ))} -
- ); -} diff --git a/client/src/components/annotations/AnnotationToolbar.test.tsx b/client/src/components/annotations/AnnotationToolbar.test.tsx deleted file mode 100644 index 6ad1155..0000000 --- a/client/src/components/annotations/AnnotationToolbar.test.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { api } from '@/lib/tauri'; -import { AnnotationToolbar } from './AnnotationToolbar'; - -// Mock the store -vi.mock('@/store', () => ({ - useStore: vi.fn(() => ({ - addAnnotation: vi.fn(), - })), -})); - -// Mock the tauri API -vi.mock('@/lib/tauri', () => ({ - api: { - annotations: { - add: vi.fn(), - }, - }, -})); - -import { useStore } from '@/store'; - -const mockUseStore = vi.mocked(useStore); -const mockAddAnnotation = vi.fn(); - -const defaultProps = { - meetingId: 'meeting-123', - segmentId: 1, - startTime: 10.0, - endTime: 15.0, -}; - -describe('AnnotationToolbar', () => { - beforeEach(() => { - vi.clearAllMocks(); - mockUseStore.mockReturnValue({ - addAnnotation: mockAddAnnotation, - }); - }); - - describe('rendering', () => { - it('should render all annotation type buttons', () => { - render(); - - expect(screen.getByTitle('Action')).toBeInTheDocument(); - expect(screen.getByTitle('Decision')).toBeInTheDocument(); - expect(screen.getByTitle('Note')).toBeInTheDocument(); - expect(screen.getByTitle('Risk')).toBeInTheDocument(); - }); - - it('should not show note input initially', () => { - render(); - - expect(screen.queryByPlaceholderText('Add note...')).not.toBeInTheDocument(); - }); - }); - - describe('quick annotations (action, decision, risk)', () => { - it('should add action item annotation', async () => { - const mockAnnotation = { - id: 'ann-1', - meeting_id: 'meeting-123', - annotation_type: 'action_item', - text: '', - start_time: 10.0, - end_time: 15.0, - segment_ids: [1], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render(); - const actionButton = screen.getByTitle('Action'); - fireEvent.click(actionButton); - - await waitFor(() => { - expect(api.annotations.add).toHaveBeenCalledWith( - 'meeting-123', - 'action_item', - '', - 10.0, - 15.0, - [1] - ); - }); - - await waitFor(() => { - expect(mockAddAnnotation).toHaveBeenCalledWith(mockAnnotation); - }); - }); - - it('should add decision annotation', async () => { - const mockAnnotation = { - id: 'ann-2', - meeting_id: 'meeting-123', - annotation_type: 'decision', - text: '', - start_time: 10.0, - end_time: 15.0, - segment_ids: [1], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render(); - const decisionButton = screen.getByTitle('Decision'); - fireEvent.click(decisionButton); - - await waitFor(() => { - expect(api.annotations.add).toHaveBeenCalledWith( - 'meeting-123', - 'decision', - '', - 10.0, - 15.0, - [1] - ); - }); - }); - - it('should add risk annotation', async () => { - const mockAnnotation = { - id: 'ann-3', - meeting_id: 'meeting-123', - annotation_type: 'risk', - text: '', - start_time: 10.0, - end_time: 15.0, - segment_ids: [1], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render(); - const riskButton = screen.getByTitle('Risk'); - fireEvent.click(riskButton); - - await waitFor(() => { - expect(api.annotations.add).toHaveBeenCalledWith( - 'meeting-123', - 'risk', - '', - 10.0, - 15.0, - [1] - ); - }); - }); - }); - - describe('note annotation with text input', () => { - it('should show text input when clicking Note button', async () => { - render(); - - const noteButton = screen.getByTitle('Note'); - fireEvent.click(noteButton); - - expect(screen.getByPlaceholderText('Add note...')).toBeInTheDocument(); - }); - - it('should submit note on Enter key', async () => { - const user = userEvent.setup(); - const mockAnnotation = { - id: 'ann-4', - meeting_id: 'meeting-123', - annotation_type: 'note', - text: 'Important note', - start_time: 10.0, - end_time: 15.0, - segment_ids: [1], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render(); - - // Click Note to show input - const noteButton = screen.getByTitle('Note'); - fireEvent.click(noteButton); - - // Type and submit - const input = screen.getByPlaceholderText('Add note...'); - await user.type(input, 'Important note{enter}'); - - await waitFor(() => { - expect(api.annotations.add).toHaveBeenCalledWith( - 'meeting-123', - 'note', - 'Important note', - 10.0, - 15.0, - [1] - ); - }); - }); - - it('should close input on Escape key', async () => { - const user = userEvent.setup(); - render(); - - // Click Note to show input - const noteButton = screen.getByTitle('Note'); - fireEvent.click(noteButton); - - const input = screen.getByPlaceholderText('Add note...'); - await user.type(input, 'Some text{escape}'); - - expect(screen.queryByPlaceholderText('Add note...')).not.toBeInTheDocument(); - }); - - it('should not submit empty note', async () => { - const user = userEvent.setup(); - render(); - - // Click Note to show input - const noteButton = screen.getByTitle('Note'); - fireEvent.click(noteButton); - - // Try to submit empty - const input = screen.getByPlaceholderText('Add note...'); - await user.type(input, '{enter}'); - - expect(api.annotations.add).not.toHaveBeenCalled(); - }); - - it('should trim whitespace from note text', async () => { - const user = userEvent.setup(); - const mockAnnotation = { - id: 'ann-5', - meeting_id: 'meeting-123', - annotation_type: 'note', - text: 'Trimmed note', - start_time: 10.0, - end_time: 15.0, - segment_ids: [1], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render(); - - const noteButton = screen.getByTitle('Note'); - fireEvent.click(noteButton); - - const input = screen.getByPlaceholderText('Add note...'); - await user.type(input, ' Trimmed note {enter}'); - - await waitFor(() => { - expect(api.annotations.add).toHaveBeenCalledWith( - 'meeting-123', - 'note', - 'Trimmed note', - 10.0, - 15.0, - [1] - ); - }); - }); - - it('should close input after successful submission', async () => { - const user = userEvent.setup(); - const mockAnnotation = { - id: 'ann-6', - meeting_id: 'meeting-123', - annotation_type: 'note', - text: 'Test note', - start_time: 10.0, - end_time: 15.0, - segment_ids: [1], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render(); - - const noteButton = screen.getByTitle('Note'); - fireEvent.click(noteButton); - - const input = screen.getByPlaceholderText('Add note...'); - await user.type(input, 'Test note{enter}'); - - await waitFor(() => { - expect(screen.queryByPlaceholderText('Add note...')).not.toBeInTheDocument(); - }); - }); - }); - - describe('loading state', () => { - it('should disable buttons while loading', async () => { - // Make add hang to simulate loading - vi.mocked(api.annotations.add).mockImplementation( - () => new Promise(() => {}) // Never resolves - ); - - render(); - const actionButton = screen.getByTitle('Action'); - fireEvent.click(actionButton); - - // All buttons should be disabled - await waitFor(() => { - expect(screen.getByTitle('Action')).toBeDisabled(); - expect(screen.getByTitle('Decision')).toBeDisabled(); - expect(screen.getByTitle('Note')).toBeDisabled(); - expect(screen.getByTitle('Risk')).toBeDisabled(); - }); - }); - }); - - describe('error handling', () => { - it('should handle API error gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(api.annotations.add).mockRejectedValue(new Error('Server error')); - - render(); - const actionButton = screen.getByTitle('Action'); - fireEvent.click(actionButton); - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to add annotation:', - expect.any(Error) - ); - }); - - // Buttons should be re-enabled after error - await waitFor(() => { - expect(screen.getByTitle('Action')).not.toBeDisabled(); - }); - - consoleSpy.mockRestore(); - }); - }); - - describe('props usage', () => { - it('should use correct meeting ID', async () => { - const mockAnnotation = { - id: 'ann-7', - meeting_id: 'different-meeting', - annotation_type: 'action_item', - text: '', - start_time: 0, - end_time: 5, - segment_ids: [42], - created_at: Date.now() / 1000, - }; - vi.mocked(api.annotations.add).mockResolvedValue(mockAnnotation); - - render( - - ); - - const actionButton = screen.getByTitle('Action'); - fireEvent.click(actionButton); - - await waitFor(() => { - expect(api.annotations.add).toHaveBeenCalledWith( - 'different-meeting', - 'action_item', - '', - 0, - 5, - [42] - ); - }); - }); - }); -}); diff --git a/client/src/components/annotations/AnnotationToolbar.tsx b/client/src/components/annotations/AnnotationToolbar.tsx deleted file mode 100644 index 0f640b1..0000000 --- a/client/src/components/annotations/AnnotationToolbar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { AlertCircle, AlertTriangle, CheckSquare, Plus, StickyNote } from 'lucide-react'; -import { useState } from 'react'; -import { api } from '@/lib/tauri'; -import { useStore } from '@/store'; -import type { AnnotationType } from '@/types/annotation'; - -interface AnnotationToolbarProps { - meetingId: string; - segmentId: number; - startTime: number; - endTime: number; -} - -const ANNOTATION_TYPES: Array<{ - type: AnnotationType; - icon: typeof CheckSquare; - label: string; - color: string; -}> = [ - { type: 'action_item', icon: CheckSquare, label: 'Action', color: 'text-green-500' }, - { type: 'decision', icon: AlertCircle, label: 'Decision', color: 'text-purple-500' }, - { type: 'note', icon: StickyNote, label: 'Note', color: 'text-blue-500' }, - { type: 'risk', icon: AlertTriangle, label: 'Risk', color: 'text-red-500' }, -]; - -export function AnnotationToolbar({ - meetingId, - segmentId, - startTime, - endTime, -}: AnnotationToolbarProps) { - const { addAnnotation } = useStore(); - const [showNoteInput, setShowNoteInput] = useState(false); - const [noteText, setNoteText] = useState(''); - const [loading, setLoading] = useState(false); - - const handleAddAnnotation = async (type: AnnotationType, text?: string) => { - setLoading(true); - try { - const annotation = await api.annotations.add( - meetingId, - type, - text ?? '', - startTime, - endTime, - [segmentId], - ); - addAnnotation(annotation); - setShowNoteInput(false); - setNoteText(''); - } catch (error) { - console.error('Failed to add annotation:', error); - } finally { - setLoading(false); - } - }; - - return ( -
- {ANNOTATION_TYPES.map(({ type, icon: Icon, label, color }) => ( - - ))} - - {showNoteInput && ( -
- setNoteText(e.target.value)} - placeholder="Add note..." - className="w-32 rounded border border-border bg-background px-2 py-1 text-xs" - onKeyDown={(e) => { - if (e.key === 'Enter' && noteText.trim()) { - handleAddAnnotation('note', noteText.trim()); - } - if (e.key === 'Escape') { - setShowNoteInput(false); - setNoteText(''); - } - }} - /> - -
- )} -
- ); -} diff --git a/client/src/components/annotations/index.ts b/client/src/components/annotations/index.ts deleted file mode 100644 index 9a27986..0000000 --- a/client/src/components/annotations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { AnnotationBadge } from './AnnotationBadge'; -export { AnnotationList } from './AnnotationList'; -export { AnnotationToolbar } from './AnnotationToolbar'; diff --git a/client/src/components/connection/ConnectionPanel.test.tsx b/client/src/components/connection/ConnectionPanel.test.tsx deleted file mode 100644 index 74dc53e..0000000 --- a/client/src/components/connection/ConnectionPanel.test.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { api } from '@/lib/tauri'; -import { ConnectionPanel } from './ConnectionPanel'; - -// Mock the store -vi.mock('@/store', () => ({ - useStore: vi.fn(), -})); - -// Mock the tauri API -vi.mock('@/lib/tauri', () => ({ - api: { - connection: { - connect: vi.fn(), - disconnect: vi.fn(), - }, - }, -})); - -import { useStore } from '@/store'; - -const mockUseStore = vi.mocked(useStore); - -describe('ConnectionPanel', () => { - const mockSetConnectionState = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('when disconnected', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - connected: false, - serverAddress: 'localhost:50051', - serverInfo: null, - setConnectionState: mockSetConnectionState, - }); - }); - - it('should show disconnected status', () => { - render(); - expect(screen.getByText('Disconnected')).toBeInTheDocument(); - }); - - it('should show connect button', () => { - render(); - expect(screen.getByTitle('Connect to server')).toBeInTheDocument(); - }); - - it('should call connect when clicking connect button', async () => { - const mockServerInfo = { - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - }; - vi.mocked(api.connection.connect).mockResolvedValue(mockServerInfo); - - render(); - const connectButton = screen.getByTitle('Connect to server'); - fireEvent.click(connectButton); - - await waitFor(() => { - expect(api.connection.connect).toHaveBeenCalledWith('localhost:50051'); - }); - }); - - it('should update connection state on successful connect', async () => { - const mockServerInfo = { - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - }; - vi.mocked(api.connection.connect).mockResolvedValue(mockServerInfo); - - render(); - const connectButton = screen.getByTitle('Connect to server'); - fireEvent.click(connectButton); - - await waitFor(() => { - expect(mockSetConnectionState).toHaveBeenCalledWith( - true, - 'localhost:50051', - mockServerInfo - ); - }); - }); - - it('should handle connection failure', async () => { - vi.mocked(api.connection.connect).mockRejectedValue(new Error('Connection failed')); - - render(); - const connectButton = screen.getByTitle('Connect to server'); - fireEvent.click(connectButton); - - await waitFor(() => { - expect(mockSetConnectionState).toHaveBeenCalledWith(false, 'localhost:50051'); - }); - }); - }); - - describe('when connected', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - connected: true, - serverAddress: 'localhost:50051', - serverInfo: { - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - }, - setConnectionState: mockSetConnectionState, - }); - }); - - it('should show connected status', () => { - render(); - expect(screen.getByText('Connected')).toBeInTheDocument(); - }); - - it('should show disconnect button', () => { - render(); - expect(screen.getByTitle('Disconnect')).toBeInTheDocument(); - }); - - it('should show server address', () => { - render(); - expect(screen.getByText('localhost:50051')).toBeInTheDocument(); - }); - - it('should call disconnect when clicking disconnect button', async () => { - vi.mocked(api.connection.disconnect).mockResolvedValue(undefined); - - render(); - const disconnectButton = screen.getByTitle('Disconnect'); - fireEvent.click(disconnectButton); - - await waitFor(() => { - expect(api.connection.disconnect).toHaveBeenCalled(); - }); - }); - - it('should update connection state on disconnect', async () => { - vi.mocked(api.connection.disconnect).mockResolvedValue(undefined); - - render(); - const disconnectButton = screen.getByTitle('Disconnect'); - fireEvent.click(disconnectButton); - - await waitFor(() => { - expect(mockSetConnectionState).toHaveBeenCalledWith(false, 'localhost:50051'); - }); - }); - }); -}); diff --git a/client/src/components/connection/ConnectionPanel.tsx b/client/src/components/connection/ConnectionPanel.tsx deleted file mode 100644 index 4a0775e..0000000 --- a/client/src/components/connection/ConnectionPanel.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { RefreshCw, Wifi, WifiOff } from 'lucide-react'; -import { useState } from 'react'; -import { api } from '@/lib/tauri'; -import { useStore } from '@/store'; - -export function ConnectionPanel() { - const { connected, serverAddress, serverInfo, setConnectionState } = useStore(); - const [connecting, setConnecting] = useState(false); - - const handleConnect = async () => { - setConnecting(true); - try { - const info = await api.connection.connect(serverAddress); - setConnectionState(true, serverAddress, info); - } catch (error) { - console.error('Connection failed:', error); - setConnectionState(false, serverAddress); - } finally { - setConnecting(false); - } - }; - - const handleDisconnect = async () => { - try { - await api.connection.disconnect(); - setConnectionState(false, serverAddress); - } catch (error) { - console.error('Disconnect failed:', error); - } - }; - - return ( -
- {/* Status indicator */} -
- {connected ? ( - <> - - Connected - - ) : ( - <> - - Disconnected - - )} -
- - {/* Reconnect button (shown when disconnected) */} - {!connected && ( - - )} - - {/* Disconnect button (shown when connected) */} - {connected && ( - - )} - - {/* Server info */} - {connected && serverInfo && ( - - {serverAddress} - - )} -
- ); -} diff --git a/client/src/components/meetings/MeetingLibrary.tsx b/client/src/components/meetings/MeetingLibrary.tsx deleted file mode 100644 index 585e503..0000000 --- a/client/src/components/meetings/MeetingLibrary.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Calendar, Clock, Download, Loader2, MessageSquare, Search, Trash2 } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { api } from '@/lib/tauri'; -import { formatDateTime, formatTime } from '@/lib/utils'; -import { useStore } from '@/store'; -import type { MeetingInfo } from '@/types'; - -export function MeetingLibrary() { - const { meetings, setMeetings, setSelectedMeeting, setCurrentMeeting, setViewMode } = useStore(); - const [loading, setLoading] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [selectedId, setSelectedId] = useState(null); - - useEffect(() => { - loadMeetings(); - }, []); - - const loadMeetings = async () => { - setLoading(true); - try { - const [meetingList] = await api.meetings.list( - ['completed', 'stopped'], - 100, - 0, - true - ); - setMeetings(meetingList); - } catch (error) { - console.error('Failed to load meetings:', error); - } finally { - setLoading(false); - } - }; - - const handleSelectMeeting = async (meeting: MeetingInfo) => { - setSelectedId(meeting.id); - try { - const details = await api.meetings.select(meeting.id); - setSelectedMeeting(details.meeting); - setCurrentMeeting(details.meeting); - setViewMode('recording'); - } catch (error) { - console.error('Failed to select meeting:', error); - } finally { - setSelectedId(null); - } - }; - - const handleDeleteMeeting = async (meetingId: string, e: React.MouseEvent) => { - e.stopPropagation(); - if (!confirm('Are you sure you want to delete this meeting?')) return; - - try { - await api.meetings.delete(meetingId); - setMeetings(meetings.filter((m) => m.id !== meetingId)); - } catch (error) { - console.error('Failed to delete meeting:', error); - } - }; - - const handleExport = async (meeting: MeetingInfo, format: 'markdown' | 'html', e: React.MouseEvent) => { - e.stopPropagation(); - try { - const result = await api.export.transcript(meeting.id, format); - const fileName = `${meeting.title.replace(/[^a-z0-9]/gi, '_')}.${result.file_extension}`; - await api.export.saveFile(result.content, fileName, result.file_extension); - } catch (error) { - console.error('Failed to export:', error); - } - }; - - const filteredMeetings = meetings.filter((m) => - m.title.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - return ( -
- {/* Search bar */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-2 rounded-lg bg-muted border border-border focus:border-primary focus:outline-none" - /> -
- -
- - {/* Meeting list */} - {loading && meetings.length === 0 ? ( -
- -
- ) : filteredMeetings.length === 0 ? ( -
- {searchQuery ? 'No matching meetings found' : 'No meetings yet'} -
- ) : ( -
- {filteredMeetings.map((meeting) => ( -
handleSelectMeeting(meeting)} - className={`p-4 rounded-lg bg-muted/50 hover:bg-muted cursor-pointer transition-colors ${ - selectedId === meeting.id ? 'opacity-50' : '' - }`} - > -
-
-

{meeting.title}

-
- - - {formatDateTime(meeting.created_at)} - - - - {formatTime(meeting.duration_seconds)} - - - - {meeting.segment_count} segments - -
-
- - {/* Actions */} -
- - -
-
-
- ))} -
- )} -
- ); -} diff --git a/client/src/components/playback/PlaybackControls.test.tsx b/client/src/components/playback/PlaybackControls.test.tsx deleted file mode 100644 index e6e8e9e..0000000 --- a/client/src/components/playback/PlaybackControls.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { api } from '@/lib/tauri'; -import { PlaybackControls } from './PlaybackControls'; - -// Mock the store -vi.mock('@/store', () => ({ - useStore: vi.fn(), -})); - -// Mock the tauri API -vi.mock('@/lib/tauri', () => ({ - api: { - playback: { - play: vi.fn(), - pause: vi.fn(), - stop: vi.fn(), - seek: vi.fn(), - }, - }, -})); - -import { useStore } from '@/store'; - -const mockUseStore = vi.mocked(useStore); - -describe('PlaybackControls', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('when stopped', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - playbackState: 'stopped', - playbackPosition: 0, - playbackDuration: 120, - }); - }); - - it('should show play button', () => { - render(); - // Play icon should be visible (not Pause) - const playButton = screen.getAllByRole('button')[0]; - expect(playButton).toBeInTheDocument(); - }); - - it('should display time as 00:00', () => { - render(); - expect(screen.getByText('00:00')).toBeInTheDocument(); - }); - - it('should display duration', () => { - render(); - expect(screen.getByText('02:00')).toBeInTheDocument(); - }); - - it('should call play when clicking play button', async () => { - vi.mocked(api.playback.play).mockResolvedValue(undefined); - - render(); - const playButton = screen.getAllByRole('button')[0]; - fireEvent.click(playButton); - - await waitFor(() => { - expect(api.playback.play).toHaveBeenCalled(); - }); - }); - - it('should have disabled stop button', () => { - render(); - const stopButton = screen.getAllByRole('button')[1]; - expect(stopButton).toBeDisabled(); - }); - }); - - describe('when playing', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - playbackState: 'playing', - playbackPosition: 30.5, - playbackDuration: 120, - }); - }); - - it('should show pause button', () => { - render(); - // Pause icon should be visible when playing - const pauseButton = screen.getAllByRole('button')[0]; - expect(pauseButton).toBeInTheDocument(); - }); - - it('should display current position', () => { - render(); - expect(screen.getByText('00:30')).toBeInTheDocument(); - }); - - it('should call pause when clicking pause button', async () => { - vi.mocked(api.playback.pause).mockResolvedValue(undefined); - - render(); - const pauseButton = screen.getAllByRole('button')[0]; - fireEvent.click(pauseButton); - - await waitFor(() => { - expect(api.playback.pause).toHaveBeenCalled(); - }); - }); - - it('should have enabled stop button', () => { - render(); - const stopButton = screen.getAllByRole('button')[1]; - expect(stopButton).not.toBeDisabled(); - }); - - it('should call stop when clicking stop button', async () => { - vi.mocked(api.playback.stop).mockResolvedValue(undefined); - - render(); - const stopButton = screen.getAllByRole('button')[1]; - fireEvent.click(stopButton); - - await waitFor(() => { - expect(api.playback.stop).toHaveBeenCalled(); - }); - }); - }); - - describe('when paused', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - playbackState: 'paused', - playbackPosition: 45, - playbackDuration: 120, - }); - }); - - it('should show play button (not pause)', () => { - render(); - const playButton = screen.getAllByRole('button')[0]; - expect(playButton).toBeInTheDocument(); - }); - - it('should preserve position display', () => { - render(); - expect(screen.getByText('00:45')).toBeInTheDocument(); - }); - - it('should have enabled stop button', () => { - render(); - const stopButton = screen.getAllByRole('button')[1]; - expect(stopButton).not.toBeDisabled(); - }); - }); - - describe('seek functionality', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - playbackState: 'playing', - playbackPosition: 30, - playbackDuration: 120, - }); - }); - - it('should call seek when slider changes', async () => { - vi.mocked(api.playback.seek).mockResolvedValue(undefined); - - render(); - const slider = screen.getByRole('slider'); - fireEvent.change(slider, { target: { value: '60' } }); - - await waitFor(() => { - expect(api.playback.seek).toHaveBeenCalledWith(60); - }); - }); - - it('should have correct slider range', () => { - render(); - const slider = screen.getByRole('slider'); - expect(slider).toHaveAttribute('min', '0'); - expect(slider).toHaveAttribute('max', '120'); - }); - }); - - describe('error handling', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - playbackState: 'stopped', - playbackPosition: 0, - playbackDuration: 120, - }); - }); - - it('should handle play error gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(api.playback.play).mockRejectedValue(new Error('Playback failed')); - - render(); - const playButton = screen.getAllByRole('button')[0]; - fireEvent.click(playButton); - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith('Failed to play:', expect.any(Error)); - }); - - consoleSpy.mockRestore(); - }); - - it('should handle seek error gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(api.playback.seek).mockRejectedValue(new Error('Seek failed')); - - render(); - const slider = screen.getByRole('slider'); - fireEvent.change(slider, { target: { value: '60' } }); - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith('Failed to seek:', expect.any(Error)); - }); - - consoleSpy.mockRestore(); - }); - }); - - describe('edge cases', () => { - it('should handle zero duration', () => { - mockUseStore.mockReturnValue({ - playbackState: 'stopped', - playbackPosition: 0, - playbackDuration: 0, - }); - - render(); - expect(screen.getAllByText('00:00')).toHaveLength(2); - }); - - it('should handle very long duration', () => { - mockUseStore.mockReturnValue({ - playbackState: 'stopped', - playbackPosition: 0, - playbackDuration: 7200, // 2 hours - }); - - render(); - // Format may be "2:00:00" or "02:00:00" depending on implementation - expect(screen.getByText(/2:00:00$/)).toBeInTheDocument(); - }); - - it('should handle position beyond duration', () => { - mockUseStore.mockReturnValue({ - playbackState: 'playing', - playbackPosition: 150, - playbackDuration: 120, - }); - - render(); - // Should still render without crashing - expect(screen.getByText('02:30')).toBeInTheDocument(); - }); - }); -}); diff --git a/client/src/components/playback/PlaybackControls.tsx b/client/src/components/playback/PlaybackControls.tsx deleted file mode 100644 index 36789c6..0000000 --- a/client/src/components/playback/PlaybackControls.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Pause, Play, Square } from 'lucide-react'; -import { api } from '@/lib/tauri'; -import { formatTime } from '@/lib/utils'; -import { useStore } from '@/store'; - -export function PlaybackControls() { - const { playbackState, playbackPosition, playbackDuration } = useStore(); - - const handlePlay = async () => { - try { - await api.playback.play(); - } catch (error) { - console.error('Failed to play:', error); - } - }; - - const handlePause = async () => { - try { - await api.playback.pause(); - } catch (error) { - console.error('Failed to pause:', error); - } - }; - - const handleStop = async () => { - try { - await api.playback.stop(); - } catch (error) { - console.error('Failed to stop:', error); - } - }; - - const handleSeek = async (e: React.ChangeEvent) => { - const position = parseFloat(e.target.value); - try { - await api.playback.seek(position); - } catch (error) { - console.error('Failed to seek:', error); - } - }; - - const isPlaying = playbackState === 'playing'; - const isStopped = playbackState === 'stopped'; - - return ( -
- {/* Play/Pause button */} - - - {/* Stop button */} - - - {/* Timeline */} -
- - {formatTime(playbackPosition)} - - - - {formatTime(playbackDuration)} - -
-
- ); -} diff --git a/client/src/components/recording/RecordingPanel.test.tsx b/client/src/components/recording/RecordingPanel.test.tsx deleted file mode 100644 index a897adc..0000000 --- a/client/src/components/recording/RecordingPanel.test.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { api } from '@/lib/tauri'; -import { RecordingPanel } from './RecordingPanel'; - -// Mock child components to isolate tests -vi.mock('./VuMeter', () => ({ - VuMeter: ({ level }: { level: number }) =>
Level: {level}
, -})); - -vi.mock('./RecordingTimer', () => ({ - RecordingTimer: ({ seconds }: { seconds: number }) =>
{seconds}s
, -})); - -vi.mock('@/components/playback/PlaybackControls', () => ({ - PlaybackControls: () =>
Playback Controls
, -})); - -// Mock the store -vi.mock('@/store', () => ({ - useStore: vi.fn(), -})); - -// Mock the tauri API -vi.mock('@/lib/tauri', () => ({ - api: { - recording: { - start: vi.fn(), - stop: vi.fn(), - }, - }, -})); - -import { useStore } from '@/store'; - -const mockUseStore = vi.mocked(useStore); -const mockSetRecording = vi.fn(); -const mockSetCurrentMeeting = vi.fn(); -const mockClearTranscript = vi.fn(); - -const defaultStoreState = { - recording: false, - currentMeeting: null, - connected: true, - audioLevel: 0.5, - elapsedSeconds: 0, - setRecording: mockSetRecording, - setCurrentMeeting: mockSetCurrentMeeting, - clearTranscript: mockClearTranscript, -}; - -describe('RecordingPanel', () => { - beforeEach(() => { - vi.clearAllMocks(); - mockUseStore.mockReturnValue(defaultStoreState); - }); - - describe('when disconnected', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - connected: false, - }); - }); - - it('should show disabled start button', () => { - render(); - const startButton = screen.getByRole('button', { name: /start recording/i }); - expect(startButton).toBeDisabled(); - }); - - it('should show VU meter', () => { - render(); - expect(screen.getByTestId('vu-meter')).toBeInTheDocument(); - }); - }); - - describe('when connected and not recording', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - connected: true, - recording: false, - }); - }); - - it('should show enabled Start Recording button', () => { - render(); - const startButton = screen.getByRole('button', { name: /start recording/i }); - expect(startButton).not.toBeDisabled(); - }); - - it('should not show recording timer', () => { - render(); - expect(screen.queryByTestId('timer')).not.toBeInTheDocument(); - }); - - it('should not show playback controls without meeting', () => { - render(); - expect(screen.queryByTestId('playback-controls')).not.toBeInTheDocument(); - }); - - it('should show playback controls with meeting', () => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - currentMeeting: { id: 'meeting-1', title: 'Test Meeting' }, - }); - render(); - expect(screen.getByTestId('playback-controls')).toBeInTheDocument(); - }); - }); - - describe('start recording flow', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - connected: true, - recording: false, - }); - }); - - it('should call api.recording.start on click', async () => { - const mockMeeting = { - id: 'meeting-123', - title: 'Meeting 1/1/2025', - state: 'recording', - }; - vi.mocked(api.recording.start).mockResolvedValue(mockMeeting as any); - - render(); - const startButton = screen.getByRole('button', { name: /start recording/i }); - fireEvent.click(startButton); - - await waitFor(() => { - expect(api.recording.start).toHaveBeenCalled(); - }); - }); - - it('should update store state after successful start', async () => { - const mockMeeting = { - id: 'meeting-123', - title: 'Test Meeting', - state: 'recording', - }; - vi.mocked(api.recording.start).mockResolvedValue(mockMeeting as any); - - render(); - const startButton = screen.getByRole('button', { name: /start recording/i }); - fireEvent.click(startButton); - - await waitFor(() => { - expect(mockSetCurrentMeeting).toHaveBeenCalledWith(mockMeeting); - expect(mockSetRecording).toHaveBeenCalledWith(true); - expect(mockClearTranscript).toHaveBeenCalled(); - }); - }); - - it('should show loading state while starting', async () => { - vi.mocked(api.recording.start).mockImplementation( - () => new Promise((resolve) => setTimeout(() => resolve({} as any), 100)) - ); - - render(); - const startButton = screen.getByRole('button', { name: /start recording/i }); - fireEvent.click(startButton); - - // Button should be disabled during loading - expect(startButton).toBeDisabled(); - }); - - it('should handle start error gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(api.recording.start).mockRejectedValue(new Error('Start failed')); - - render(); - const startButton = screen.getByRole('button', { name: /start recording/i }); - fireEvent.click(startButton); - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith('Failed to start recording:', expect.any(Error)); - }); - - // Button should be re-enabled - await waitFor(() => { - expect(startButton).not.toBeDisabled(); - }); - - consoleSpy.mockRestore(); - }); - }); - - describe('when recording', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - connected: true, - recording: true, - elapsedSeconds: 45, - currentMeeting: { id: 'meeting-1', title: 'Test' }, - }); - }); - - it('should show Stop button instead of Start', () => { - render(); - expect(screen.getByRole('button', { name: /stop/i })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /start recording/i })).not.toBeInTheDocument(); - }); - - it('should show recording timer', () => { - render(); - expect(screen.getByTestId('timer')).toBeInTheDocument(); - expect(screen.getByText('45s')).toBeInTheDocument(); - }); - - it('should not show playback controls while recording', () => { - render(); - expect(screen.queryByTestId('playback-controls')).not.toBeInTheDocument(); - }); - }); - - describe('stop recording flow', () => { - beforeEach(() => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - connected: true, - recording: true, - currentMeeting: { id: 'meeting-1', title: 'Test' }, - }); - }); - - it('should call api.recording.stop on click', async () => { - const mockMeeting = { - id: 'meeting-123', - title: 'Test Meeting', - state: 'stopped', - }; - vi.mocked(api.recording.stop).mockResolvedValue(mockMeeting as any); - - render(); - const stopButton = screen.getByRole('button', { name: /stop/i }); - fireEvent.click(stopButton); - - await waitFor(() => { - expect(api.recording.stop).toHaveBeenCalled(); - }); - }); - - it('should update store state after successful stop', async () => { - const mockMeeting = { - id: 'meeting-123', - title: 'Test Meeting', - state: 'stopped', - }; - vi.mocked(api.recording.stop).mockResolvedValue(mockMeeting as any); - - render(); - const stopButton = screen.getByRole('button', { name: /stop/i }); - fireEvent.click(stopButton); - - await waitFor(() => { - expect(mockSetCurrentMeeting).toHaveBeenCalledWith(mockMeeting); - expect(mockSetRecording).toHaveBeenCalledWith(false); - }); - }); - - it('should handle stop error gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(api.recording.stop).mockRejectedValue(new Error('Stop failed')); - - render(); - const stopButton = screen.getByRole('button', { name: /stop/i }); - fireEvent.click(stopButton); - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith('Failed to stop recording:', expect.any(Error)); - }); - - consoleSpy.mockRestore(); - }); - }); - - describe('VU meter integration', () => { - it('should pass audio level to VU meter', () => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - audioLevel: 0.75, - }); - - render(); - expect(screen.getByText('Level: 0.75')).toBeInTheDocument(); - }); - - it('should handle zero audio level', () => { - mockUseStore.mockReturnValue({ - ...defaultStoreState, - audioLevel: 0, - }); - - render(); - expect(screen.getByText('Level: 0')).toBeInTheDocument(); - }); - }); -}); diff --git a/client/src/components/recording/RecordingPanel.tsx b/client/src/components/recording/RecordingPanel.tsx deleted file mode 100644 index 3f645a8..0000000 --- a/client/src/components/recording/RecordingPanel.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Loader2, Mic, Square } from 'lucide-react'; -import { useState } from 'react'; -import { PlaybackControls } from '@/components/playback/PlaybackControls'; -import { api } from '@/lib/tauri'; -import { useStore } from '@/store'; -import { RecordingTimer } from './RecordingTimer'; -import { VuMeter } from './VuMeter'; - -export function RecordingPanel() { - const { - recording, - currentMeeting, - connected, - audioLevel, - elapsedSeconds, - setRecording, - setCurrentMeeting, - clearTranscript, - } = useStore(); - - const [loading, setLoading] = useState(false); - - const handleStartRecording = async () => { - setLoading(true); - try { - const meeting = await api.recording.start(`Meeting ${new Date().toLocaleString()}`); - setCurrentMeeting(meeting); - setRecording(true); - clearTranscript(); - } catch (error) { - console.error('Failed to start recording:', error); - } finally { - setLoading(false); - } - }; - - const handleStopRecording = async () => { - setLoading(true); - try { - const meeting = await api.recording.stop(); - setCurrentMeeting(meeting); - setRecording(false); - } catch (error) { - console.error('Failed to stop recording:', error); - } finally { - setLoading(false); - } - }; - - const canRecord = connected && !recording; - const canStop = recording; - - return ( -
-
- {/* Recording controls */} -
- {!recording ? ( - - ) : ( - - )} -
- - {/* VU Meter */} -
- -
- - {/* Recording timer */} - {recording && } -
- - {/* Playback controls (when not recording and have a meeting) */} - {!recording && currentMeeting && ( -
- -
- )} -
- ); -} diff --git a/client/src/components/recording/RecordingTimer.tsx b/client/src/components/recording/RecordingTimer.tsx deleted file mode 100644 index d79020c..0000000 --- a/client/src/components/recording/RecordingTimer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { cn, formatTime } from '@/lib/utils'; - -interface RecordingTimerProps { - /** Elapsed seconds to display */ - seconds: number; - /** Whether to show the pulsing recording indicator */ - showIndicator?: boolean; - /** Additional CSS classes */ - className?: string; -} - -/** - * Recording timer display component. - * - * Displays elapsed time in MM:SS or HH:MM:SS format with an optional - * pulsing red indicator to show active recording. - * - * Uses formatTime() from lib/utils for consistent time formatting. - */ -export function RecordingTimer({ - seconds, - showIndicator = true, - className, -}: RecordingTimerProps) { - return ( -
- {showIndicator && ( -
- )} - {formatTime(seconds)} -
- ); -} diff --git a/client/src/components/recording/VuMeter.test.tsx b/client/src/components/recording/VuMeter.test.tsx deleted file mode 100644 index fd494f1..0000000 --- a/client/src/components/recording/VuMeter.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; -import { VuMeter } from './VuMeter'; - -describe('VuMeter', () => { - describe('rendering', () => { - it('should render with default segments', () => { - render(); - // Default is 20 segments - const container = screen.getByText(/-?\d+ dB/).parentElement; - expect(container?.querySelectorAll('div > div').length).toBeGreaterThanOrEqual(20); - }); - - it('should render with custom segment count', () => { - render(); - const container = screen.getByText(/-?\d+ dB/).parentElement; - expect(container?.querySelectorAll('div > div').length).toBeGreaterThanOrEqual(10); - }); - - it('should display dB level', () => { - render(); - expect(screen.getByText(/dB/)).toBeInTheDocument(); - }); - }); - - describe('level display', () => { - it('should show -60 dB for zero level', () => { - render(); - expect(screen.getByText('-60 dB')).toBeInTheDocument(); - }); - - it('should show 0 dB for full level', () => { - render(); - expect(screen.getByText('0 dB')).toBeInTheDocument(); - }); - - it('should show intermediate dB values', () => { - render(); - // 20 * log10(0.1) = -20 dB - expect(screen.getByText('-20 dB')).toBeInTheDocument(); - }); - - it('should clamp values below -60 dB', () => { - render(); - expect(screen.getByText('-60 dB')).toBeInTheDocument(); - }); - }); - - describe('segment activation', () => { - it('should have no active segments at zero level', () => { - render(); - // At -60 dB, normalized level is 0, so 0 active segments - // All segments should have inactive colors - const container = screen.getByText(/-?\d+ dB/).parentElement; - const segments = container?.querySelectorAll('div > div'); - segments?.forEach((segment) => { - // Inactive segments have /30 opacity classes - expect(segment.className).toMatch(/bg-\w+-900\/30/); - }); - }); - - it('should have all segments active at full level', () => { - render(); - // At 0 dB, normalized level is 1, so all segments active - const container = screen.getByText('0 dB').parentElement; - const segments = container?.querySelectorAll('div > div'); - // All segments should have active colors (bg-X-500) - let activeCount = 0; - segments?.forEach((segment) => { - if (segment.className.match(/bg-(green|yellow|red)-500(?!\/)/) || - !segment.className.match(/\/30/)) { - activeCount++; - } - }); - expect(activeCount).toBeGreaterThan(0); - }); - }); - - describe('color zones', () => { - it('should use green for low levels', () => { - const { container } = render(); - const segments = container.querySelectorAll('.h-4'); - // First segments should be green - expect(segments[0]?.className).toMatch(/green/); - }); - - it('should use yellow for medium-high levels', () => { - render(); - // At higher levels, we should see yellow segments - const container = screen.getByText(/-?\d+ dB/).parentElement; - expect(container?.innerHTML).toMatch(/yellow/); - }); - - it('should use red for peak levels', () => { - render(); - // At full level, we should see red segments - const container = screen.getByText('0 dB').parentElement; - expect(container?.innerHTML).toMatch(/red/); - }); - }); -}); diff --git a/client/src/components/recording/VuMeter.tsx b/client/src/components/recording/VuMeter.tsx deleted file mode 100644 index 14841e5..0000000 --- a/client/src/components/recording/VuMeter.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useMemo } from 'react'; - -interface VuMeterProps { - level: number; // 0.0 to 1.0 - segments?: number; -} - -export function VuMeter({ level, segments = 20 }: VuMeterProps) { - // Convert linear level to dB for more natural display - const dbLevel = useMemo(() => { - if (level <= 0) return -60; - const db = 20 * Math.log10(level); - return Math.max(-60, Math.min(0, db)); - }, [level]); - - // Normalize to 0-1 range (-60dB to 0dB) - const normalizedLevel = (dbLevel + 60) / 60; - - // Calculate active segments - const activeSegments = Math.round(normalizedLevel * segments); - - // Segment colors: green -> yellow -> red - const getSegmentColor = (index: number): string => { - const position = index / segments; - if (position < 0.6) return 'bg-green-500'; - if (position < 0.8) return 'bg-yellow-500'; - return 'bg-red-500'; - }; - - const getInactiveColor = (index: number): string => { - const position = index / segments; - if (position < 0.6) return 'bg-green-900/30'; - if (position < 0.8) return 'bg-yellow-900/30'; - return 'bg-red-900/30'; - }; - - return ( -
- {Array.from({ length: segments }, (_, i) => ( -
- ))} - - {dbLevel.toFixed(0)} dB - -
- ); -} diff --git a/client/src/components/settings/SettingsField.tsx b/client/src/components/settings/SettingsField.tsx deleted file mode 100644 index 4171a58..0000000 --- a/client/src/components/settings/SettingsField.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface SettingsFieldProps { - label: string; - description?: string; - children: React.ReactNode; -} - -export function SettingsField({ label, description, children }: SettingsFieldProps) { - return ( -
-
- {label} - {description &&

{description}

} -
-
{children}
-
- ); -} diff --git a/client/src/components/settings/SettingsPanel.tsx b/client/src/components/settings/SettingsPanel.tsx deleted file mode 100644 index f29e4a1..0000000 --- a/client/src/components/settings/SettingsPanel.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { Brain, HardDrive, Loader2, Save, Server, Shield, Zap } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { api } from '@/lib/tauri'; -import type { SummarizationProvider, UserPreferences } from '@/types'; -import { SettingsField } from './SettingsField'; -import { SettingsSection } from './SettingsSection'; - -const DEFAULT_PREFERENCES: UserPreferences = { - serverUrl: 'localhost:50051', - dataDirectory: '', - encryptionEnabled: true, - autoStartEnabled: true, - triggerConfidenceThreshold: 0.7, - summarizationProvider: 'none', - cloudApiKey: '', - ollamaUrl: 'http://localhost:11434', -}; - -export function SettingsPanel() { - const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const loadPrefs = async () => { - try { - const prefs = await api.preferences.get(); - setPreferences(prefs); - } catch (err) { - setError(`Failed to load preferences: ${err}`); - } finally { - setLoading(false); - } - }; - loadPrefs(); - }, []); - - const handleSave = async () => { - setSaving(true); - setError(null); - try { - await api.preferences.save(preferences); - } catch (err) { - setError(`Failed to save: ${err}`); - } finally { - setSaving(false); - } - }; - - const updateField = (field: K, value: UserPreferences[K]) => { - setPreferences((prev) => ({ ...prev, [field]: value })); - }; - - if (loading) { - return ( -
- -
- ); - } - - return ( -
-
-
-

Settings

- -
- - {error && ( -
{error}
- )} - - - - updateField('serverUrl', e.target.value)} - className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm" - placeholder="localhost:50051" - /> - - - - - - updateField('dataDirectory', e.target.value)} - className="w-64 rounded-md border border-border bg-background px-3 py-1.5 text-sm" - placeholder="/path/to/data" - /> - - - - - - updateField('encryptionEnabled', v)} - /> - - - - - - updateField('autoStartEnabled', v)} - /> - - -
- - updateField('triggerConfidenceThreshold', parseFloat(e.target.value)) - } - className="w-24" - /> - - {Math.round(preferences.triggerConfidenceThreshold * 100)}% - -
-
-
- - - - - - - {preferences.summarizationProvider === 'cloud' && ( - - updateField('cloudApiKey', e.target.value)} - className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm" - placeholder="sk-..." - /> - - )} - - {preferences.summarizationProvider === 'ollama' && ( - - updateField('ollamaUrl', e.target.value)} - className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm" - placeholder="http://localhost:11434" - /> - - )} - -
-
- ); -} - -function ToggleSwitch({ - checked, - onChange, -}: { - checked: boolean; - onChange: (value: boolean) => void; -}) { - return ( - - ); -} diff --git a/client/src/components/settings/SettingsSection.tsx b/client/src/components/settings/SettingsSection.tsx deleted file mode 100644 index 14c3837..0000000 --- a/client/src/components/settings/SettingsSection.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { LucideIcon } from 'lucide-react'; - -interface SettingsSectionProps { - icon: LucideIcon; - title: string; - children: React.ReactNode; -} - -export function SettingsSection({ icon: Icon, title, children }: SettingsSectionProps) { - return ( -
-
- -

{title}

-
-
{children}
-
- ); -} diff --git a/client/src/components/settings/index.ts b/client/src/components/settings/index.ts deleted file mode 100644 index 7c0560b..0000000 --- a/client/src/components/settings/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { SettingsField } from './SettingsField'; -export { SettingsPanel } from './SettingsPanel'; -export { SettingsSection } from './SettingsSection'; diff --git a/client/src/components/summary/SummaryPanel.tsx b/client/src/components/summary/SummaryPanel.tsx deleted file mode 100644 index b9cdc6f..0000000 --- a/client/src/components/summary/SummaryPanel.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { AlertCircle, CheckCircle, Loader2, RefreshCw } from 'lucide-react'; -import { useState } from 'react'; -import { api } from '@/lib/tauri'; -import { useStore } from '@/store'; - -export function SummaryPanel() { - const { - currentMeeting, - summary, - summaryLoading, - summaryError, - segments, - setSummary, - setSummaryLoading, - setSummaryError, - } = useStore(); - - const [forceRegenerate] = useState(false); - - const handleGenerateSummary = async () => { - if (!currentMeeting) return; - - setSummaryLoading(true); - setSummaryError(null); - - try { - const result = await api.summary.generate(currentMeeting.id, forceRegenerate); - setSummary(result); - } catch (error) { - setSummaryError(String(error)); - } finally { - setSummaryLoading(false); - } - }; - - const handleCitationClick = async (segmentIds: number[]) => { - if (segmentIds.length === 0) return; - - // Find the segment and seek to it - const segment = segments.find((s) => s.segment_id === segmentIds[0]); - if (segment) { - try { - await api.playback.seek(segment.start_time); - } catch (error) { - console.error('Failed to seek:', error); - } - } - }; - - return ( -
-
-

Summary

- {currentMeeting && ( - - )} -
- - {/* Error state */} - {summaryError && ( -
- - {summaryError} -
- )} - - {/* Loading state */} - {summaryLoading && ( -
- -
- )} - - {/* No summary yet */} - {!summary && !summaryLoading && ( -
- {segments.length === 0 - ? 'Record a meeting to generate a summary' - : 'Click Generate to create a summary'} -
- )} - - {/* Summary content */} - {summary && !summaryLoading && ( -
- {/* Executive summary */} -
-

- Executive Summary -

-

{summary.executive_summary}

-
- - {/* Key points */} - {summary.key_points.length > 0 && ( -
-

- Key Points -

-
    - {summary.key_points.map((point, index) => ( -
  • - -
    -

    {point.text}

    - {point.segment_ids.length > 0 && ( - - )} -
    -
  • - ))} -
-
- )} - - {/* Action items */} - {summary.action_items.length > 0 && ( -
-

- Action Items -

-
    - {summary.action_items.map((item, index) => ( -
  • -
    -

    {item.text}

    -
    - {item.assignee && ( - - {item.assignee} - - )} - {item.priority > 0 && ( - - {item.priority === 3 - ? 'High' - : item.priority === 2 - ? 'Medium' - : 'Low'} - - )} - {item.segment_ids.length > 0 && ( - - )} -
    -
    -
  • - ))} -
-
- )} -
- )} -
- ); -} diff --git a/client/src/components/transcript/TranscriptView.tsx b/client/src/components/transcript/TranscriptView.tsx deleted file mode 100644 index 1f9a510..0000000 --- a/client/src/components/transcript/TranscriptView.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { AnnotationList, AnnotationToolbar } from '@/components/annotations'; -import { api } from '@/lib/tauri'; -import { cn, formatTime, getSpeakerColor } from '@/lib/utils'; -import { useStore } from '@/store'; - -export function TranscriptView() { - const { - segments, - partialText, - highlightedSegment, - recording, - annotations, - currentMeeting, - removeAnnotation, - } = useStore(); - const containerRef = useRef(null); - const highlightedRef = useRef(null); - - // Auto-scroll to highlighted segment - useEffect(() => { - if (highlightedRef.current && containerRef.current) { - highlightedRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); - } - }, [highlightedSegment]); - - const handleSegmentClick = async (startTime: number) => { - try { - await api.playback.seek(startTime); - } catch (error) { - console.error('Failed to seek:', error); - } - }; - - return ( -
- {segments.length === 0 && !partialText && ( -
- {recording ? 'Listening...' : 'No transcript yet'} -
- )} - -
- {segments.map((segment, index) => { - const isHighlighted = highlightedSegment === index; - const speakerColor = getSpeakerColor(segment.speaker_id); - const segmentAnnotations = annotations.filter((a) => - a.segment_ids.includes(segment.segment_id) - ); - - const handleRemoveAnnotation = async (annotationId: string) => { - try { - await api.annotations.delete(annotationId); - removeAnnotation(annotationId); - } catch (error) { - console.error('Failed to remove annotation:', error); - } - }; - - return ( -
-
handleSegmentClick(segment.start_time)} - className="cursor-pointer" - > -
- - {segment.speaker_id || 'Unknown'} - - - {formatTime(segment.start_time)} - {formatTime(segment.end_time)} - -
-

{segment.text}

-
- - - - {currentMeeting && ( -
- -
- )} -
- ); - })} - - {/* Partial text */} - {partialText && ( -
-

{partialText}

-
- )} -
-
- ); -} diff --git a/client/src/components/triggers/TriggerDialog.tsx b/client/src/components/triggers/TriggerDialog.tsx deleted file mode 100644 index 9a37ebb..0000000 --- a/client/src/components/triggers/TriggerDialog.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { AlertCircle, Clock, Play, X } from 'lucide-react'; -import { api } from '@/lib/tauri'; -import { useStore } from '@/store'; - -export function TriggerDialog() { - const { - triggerPending, - triggerDecision, - setTriggerPending, - setTriggerDecision, - setRecording, - setCurrentMeeting, - } = useStore(); - - if (!triggerPending || !triggerDecision) { - return null; - } - - const handleAccept = async () => { - try { - const meeting = await api.triggers.accept(); - setCurrentMeeting(meeting); - setRecording(true); - setTriggerPending(false); - setTriggerDecision(null); - } catch (error) { - console.error('Failed to start recording:', error); - } - }; - - const handleSnooze = async (minutes: number) => { - try { - await api.triggers.snooze(minutes); - setTriggerPending(false); - setTriggerDecision(null); - } catch (error) { - console.error('Failed to snooze:', error); - } - }; - - const handleDismiss = async () => { - try { - await api.triggers.dismiss(); - setTriggerPending(false); - setTriggerDecision(null); - } catch (error) { - console.error('Failed to dismiss:', error); - } - }; - - return ( -
-
- {/* Header */} -
-
- -
-
-

Meeting Detected

-

- {triggerDecision.detected_app - ? `${triggerDecision.detected_app} appears to be active` - : 'A meeting appears to be starting'} -

-
- -
- - {/* Confidence */} -
-
- Confidence - - {Math.round(triggerDecision.confidence * 100)}% - -
-
-
-
-
- - {/* Actions */} -
- - -
- - - -
- - -
-
-
- ); -} diff --git a/client/src/hooks/useTauriEvents.test.ts b/client/src/hooks/useTauriEvents.test.ts deleted file mode 100644 index b966d11..0000000 --- a/client/src/hooks/useTauriEvents.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useTauriEvents } from './useTauriEvents'; - -// Mock the Tauri event API -vi.mock('@tauri-apps/api/event', () => ({ - listen: vi.fn(), -})); - -// Mock the store -vi.mock('@/store', () => ({ - useStore: vi.fn(() => ({ - setAudioLevel: vi.fn(), - setElapsedSeconds: vi.fn(), - addSegment: vi.fn(), - setPartialText: vi.fn(), - setPlaybackPosition: vi.fn(), - setPlaybackState: vi.fn(), - setHighlightedSegment: vi.fn(), - setConnectionState: vi.fn(), - setTriggerDecision: vi.fn(), - setTriggerPending: vi.fn(), - })), -})); - -import { listen } from '@tauri-apps/api/event'; - -const mockListen = vi.mocked(listen); - -describe('useTauriEvents', () => { - const mockUnlisten = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - mockListen.mockResolvedValue(mockUnlisten); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should register event listeners on mount', async () => { - renderHook(() => useTauriEvents()); - - // Wait for listeners to be registered - await vi.waitFor(() => { - expect(mockListen).toHaveBeenCalled(); - }); - - // Should register listeners for various events - const registeredEvents = mockListen.mock.calls.map((call) => call[0]); - expect(registeredEvents.length).toBeGreaterThan(0); - }); - - it('should unregister listeners on unmount', async () => { - const { unmount } = renderHook(() => useTauriEvents()); - - // Wait for listeners to be set up - await vi.waitFor(() => { - expect(mockListen).toHaveBeenCalled(); - }); - - // Unmount - unmount(); - - // Wait a bit for cleanup - await vi.waitFor(() => { - // Unlisten functions should be called - expect(mockUnlisten).toHaveBeenCalled(); - }); - }); - - it('should handle listen failures gracefully', async () => { - // Make listen fail - mockListen.mockRejectedValue(new Error('Listen failed')); - - // Should not throw - expect(() => { - renderHook(() => useTauriEvents()); - }).not.toThrow(); - }); -}); diff --git a/client/src/hooks/useTauriEvents.ts b/client/src/hooks/useTauriEvents.ts deleted file mode 100644 index 7723981..0000000 --- a/client/src/hooks/useTauriEvents.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { listen, type UnlistenFn } from '@tauri-apps/api/event'; -import { useEffect, useRef } from 'react'; -import { Events, PlaybackStates } from '@/lib/constants'; -import { useStore } from '@/store'; -import type { ConnectionEvent, PlaybackState, TranscriptUpdate, TriggerDecision } from '@/types'; - -export function useTauriEvents() { - const store = useStore(); - // Use ref to avoid re-subscribing on store changes - const storeRef = useRef(store); - storeRef.current = store; - - useEffect(() => { - // Track whether the effect has been cleaned up - let cancelled = false; - // Store unlisten functions that need cleanup - const unlistenFns: UnlistenFn[] = []; - - // Register all listeners in parallel, then handle cleanup atomically - const listenerPromises = [ - // Transcript updates - listen(Events.TRANSCRIPT_UPDATE, (event) => { - if (cancelled) return; - const update = event.payload; - if (update.update_type === 'final' && update.segment) { - storeRef.current.addSegment(update.segment); - } else if (update.update_type === 'partial') { - storeRef.current.setPartialText(update.partial_text); - } - }), - - // Audio level - listen(Events.AUDIO_LEVEL, (event) => { - if (cancelled) return; - storeRef.current.setAudioLevel(event.payload); - }), - - // Playback position - listen(Events.PLAYBACK_POSITION, (event) => { - if (cancelled) return; - storeRef.current.setPlaybackPosition(event.payload); - }), - - // Playback state - listen(Events.PLAYBACK_STATE, (event) => { - if (cancelled) return; - const state = event.payload as PlaybackState; - if (Object.values(PlaybackStates).includes(state)) { - storeRef.current.setPlaybackState(state); - } - }), - - // Highlight change - listen(Events.HIGHLIGHT_CHANGE, (event) => { - if (cancelled) return; - storeRef.current.setHighlightedSegment(event.payload); - }), - - // Connection change - listen(Events.CONNECTION_CHANGE, (event) => { - if (cancelled) return; - const { connected, server_address } = event.payload; - storeRef.current.setConnectionState(connected, server_address); - }), - - // Meeting detected (trigger) - listen(Events.MEETING_DETECTED, (event) => { - if (cancelled) return; - storeRef.current.setTriggerDecision(event.payload); - storeRef.current.setTriggerPending(true); - }), - - // Recording timer - listen(Events.RECORDING_TIMER, (event) => { - if (cancelled) return; - storeRef.current.setElapsedSeconds(event.payload); - }), - ]; - - // Wait for all listeners to be registered, then store unlisten functions - // Use allSettled to handle individual listener failures gracefully - Promise.allSettled(listenerPromises).then((results) => { - const fns = results - .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') - .map((r) => r.value); - - // Log failures but don't leak listeners - results - .filter((r): r is PromiseRejectedResult => r.status === 'rejected') - .forEach((r) => console.error('Failed to register Tauri listener:', r.reason)); - - if (cancelled) { - // Component unmounted during setup - clean up immediately - fns.forEach((unlisten) => unlisten()); - } else { - // Store for cleanup on unmount - unlistenFns.push(...fns); - } - }); - - // Cleanup: mark as cancelled and clean up any already-registered listeners - return () => { - cancelled = true; - unlistenFns.forEach((unlisten) => unlisten()); - }; - }, []); // Empty deps - only setup once -} diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index 63e112c..0000000 --- a/client/src/index.css +++ /dev/null @@ -1,66 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 220 14% 10%; - --foreground: 210 20% 98%; - - --muted: 215 20% 20%; - --muted-foreground: 215 20% 65%; - - --popover: 220 14% 12%; - --popover-foreground: 210 20% 98%; - - --card: 220 14% 12%; - --card-foreground: 210 20% 98%; - - --border: 215 20% 25%; - --input: 215 20% 25%; - - --primary: 217 91% 60%; - --primary-foreground: 210 20% 98%; - - --secondary: 215 20% 20%; - --secondary-foreground: 210 20% 98%; - - --accent: 215 20% 20%; - --accent-foreground: 210 20% 98%; - - --destructive: 0 84% 60%; - --destructive-foreground: 210 20% 98%; - - --ring: 217 91% 60%; - - --radius: 0.5rem; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - font-feature-settings: "rlig" 1, "calt" 1; - } -} - -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - @apply bg-muted; -} - -::-webkit-scrollbar-thumb { - @apply bg-muted-foreground/30 rounded; -} - -::-webkit-scrollbar-thumb:hover { - @apply bg-muted-foreground/50; -} diff --git a/client/src/lib/cache.test.ts b/client/src/lib/cache.test.ts deleted file mode 100644 index 43a6209..0000000 --- a/client/src/lib/cache.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { type Cache, CacheKey, type CacheStats, getCache, resetCache } from './cache'; - -// Helper to create a test MemoryCache with controlled timing -class TestableMemoryCache implements Cache { - private entries = new Map(); - private hits = 0; - private misses = 0; - private maxItems: number; - private defaultTtlSecs: number; - - constructor(maxItems = 1000, defaultTtlSecs = 300) { - this.maxItems = maxItems; - this.defaultTtlSecs = defaultTtlSecs; - } - - async get(key: string): Promise { - const entry = this.entries.get(key); - if (!entry) { - this.misses++; - return null; - } - if (Date.now() > entry.expiresAt) { - this.entries.delete(key); - this.misses++; - return null; - } - this.hits++; - return entry.value as T; - } - - async set(key: string, value: T, ttlSecs?: number): Promise { - if (this.maxItems <= 0) return; - const ttl = ttlSecs ?? this.defaultTtlSecs; - const expiresAt = Date.now() + ttl * 1000; - this.entries.set(key, { value, expiresAt }); - } - - async delete(key: string): Promise { - return this.entries.delete(key); - } - - async deleteByPrefix(prefix: string): Promise { - const keysToDelete: string[] = []; - for (const key of this.entries.keys()) { - if (key.startsWith(prefix)) { - keysToDelete.push(key); - } - } - for (const key of keysToDelete) { - this.entries.delete(key); - } - return keysToDelete.length; - } - - async exists(key: string): Promise { - const entry = this.entries.get(key); - if (!entry) return false; - if (Date.now() > entry.expiresAt) { - this.entries.delete(key); - return false; - } - return true; - } - - async clear(): Promise { - this.entries.clear(); - this.hits = 0; - this.misses = 0; - } - - stats(): CacheStats { - return { - hits: this.hits, - misses: this.misses, - items: this.entries.size, - bytes: 0, - }; - } -} - -describe('CacheKey', () => { - describe('meeting', () => { - it('should format meeting key correctly', () => { - expect(CacheKey.meeting('123')).toBe('meeting:123'); - }); - - it('should handle UUID meeting IDs', () => { - const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; - expect(CacheKey.meeting(uuid)).toBe(`meeting:${uuid}`); - }); - }); - - describe('meetingList', () => { - it('should return consistent meeting list key', () => { - expect(CacheKey.meetingList()).toBe('meetings:list'); - }); - }); - - describe('segments', () => { - it('should format segments key correctly', () => { - expect(CacheKey.segments('meeting-123')).toBe('segments:meeting-123'); - }); - }); - - describe('summary', () => { - it('should format summary key correctly', () => { - expect(CacheKey.summary('meeting-456')).toBe('summary:meeting-456'); - }); - }); - - describe('serverInfo', () => { - it('should return consistent server info key', () => { - expect(CacheKey.serverInfo()).toBe('server:info'); - }); - }); - - describe('custom', () => { - it('should create custom keys with prefix and id', () => { - expect(CacheKey.custom('config', 'audio')).toBe('config:audio'); - expect(CacheKey.custom('user', 'preferences')).toBe('user:preferences'); - }); - }); -}); - -describe('MemoryCache', () => { - let cache: TestableMemoryCache; - - beforeEach(() => { - cache = new TestableMemoryCache(1000, 300); - }); - - describe('get/set', () => { - it('should store and retrieve values', async () => { - await cache.set('key1', { foo: 'bar' }); - const result = await cache.get('key1'); - expect(result).toEqual({ foo: 'bar' }); - }); - - it('should return null for missing keys', async () => { - const result = await cache.get('nonexistent'); - expect(result).toBeNull(); - }); - - it('should handle different value types', async () => { - await cache.set('string', 'hello'); - await cache.set('number', 42); - await cache.set('boolean', true); - await cache.set('array', [1, 2, 3]); - await cache.set('object', { nested: { value: 'test' } }); - - expect(await cache.get('string')).toBe('hello'); - expect(await cache.get('number')).toBe(42); - expect(await cache.get('boolean')).toBe(true); - expect(await cache.get('array')).toEqual([1, 2, 3]); - expect(await cache.get('object')).toEqual({ nested: { value: 'test' } }); - }); - - it('should overwrite existing keys', async () => { - await cache.set('key', 'first'); - await cache.set('key', 'second'); - expect(await cache.get('key')).toBe('second'); - }); - }); - - describe('expiration', () => { - it('should expire entries after TTL', async () => { - vi.useFakeTimers(); - await cache.set('expiring', 'value', 1); // 1 second TTL - - expect(await cache.get('expiring')).toBe('value'); - - vi.advanceTimersByTime(2000); // Advance 2 seconds - - expect(await cache.get('expiring')).toBeNull(); - vi.useRealTimers(); - }); - }); - - describe('delete', () => { - it('should delete existing keys', async () => { - await cache.set('key', 'value'); - const deleted = await cache.delete('key'); - expect(deleted).toBe(true); - expect(await cache.get('key')).toBeNull(); - }); - - it('should return false for missing keys', async () => { - const deleted = await cache.delete('nonexistent'); - expect(deleted).toBe(false); - }); - }); - - describe('deleteByPrefix', () => { - it('should delete all keys with matching prefix', async () => { - await cache.set('user:1', 'Alice'); - await cache.set('user:2', 'Bob'); - await cache.set('user:3', 'Charlie'); - await cache.set('other:1', 'Other'); - - const count = await cache.deleteByPrefix('user:'); - - expect(count).toBe(3); - expect(await cache.get('user:1')).toBeNull(); - expect(await cache.get('user:2')).toBeNull(); - expect(await cache.get('user:3')).toBeNull(); - expect(await cache.get('other:1')).toBe('Other'); - }); - - it('should return 0 when no keys match', async () => { - await cache.set('key1', 'value1'); - const count = await cache.deleteByPrefix('nonexistent:'); - expect(count).toBe(0); - }); - }); - - describe('exists', () => { - it('should return true for existing keys', async () => { - await cache.set('key', 'value'); - expect(await cache.exists('key')).toBe(true); - }); - - it('should return false for missing keys', async () => { - expect(await cache.exists('nonexistent')).toBe(false); - }); - - it('should return false for expired keys', async () => { - vi.useFakeTimers(); - await cache.set('expiring', 'value', 1); - vi.advanceTimersByTime(2000); - expect(await cache.exists('expiring')).toBe(false); - vi.useRealTimers(); - }); - }); - - describe('clear', () => { - it('should remove all entries', async () => { - await cache.set('key1', 'value1'); - await cache.set('key2', 'value2'); - await cache.set('key3', 'value3'); - - await cache.clear(); - - expect(await cache.get('key1')).toBeNull(); - expect(await cache.get('key2')).toBeNull(); - expect(await cache.get('key3')).toBeNull(); - }); - - it('should reset statistics', async () => { - await cache.set('key', 'value'); - await cache.get('key'); - await cache.get('missing'); - - await cache.clear(); - - const stats = cache.stats(); - expect(stats.hits).toBe(0); - expect(stats.misses).toBe(0); - expect(stats.items).toBe(0); - }); - }); - - describe('stats', () => { - it('should track hits and misses', async () => { - await cache.set('key1', 'value1'); - - await cache.get('key1'); // hit - await cache.get('key1'); // hit - await cache.get('missing'); // miss - - const stats = cache.stats(); - expect(stats.hits).toBe(2); - expect(stats.misses).toBe(1); - }); - - it('should track item count', async () => { - await cache.set('key1', 'value1'); - await cache.set('key2', 'value2'); - - const stats = cache.stats(); - expect(stats.items).toBe(2); - }); - }); -}); - -describe('cache module integration', () => { - beforeEach(() => { - resetCache(); - }); - - afterEach(() => { - resetCache(); - }); - - it('should create cache instance on first access', () => { - const cache = getCache(); - expect(cache).toBeDefined(); - expect(cache.get).toBeDefined(); - expect(cache.set).toBeDefined(); - }); - - it('should return same instance on subsequent accesses', () => { - const cache1 = getCache(); - const cache2 = getCache(); - expect(cache1).toBe(cache2); - }); - - it('should create new instance after reset', () => { - const cache1 = getCache(); - resetCache(); - const cache2 = getCache(); - expect(cache1).not.toBe(cache2); - }); -}); diff --git a/client/src/lib/cache.ts b/client/src/lib/cache.ts deleted file mode 100644 index b4dbce7..0000000 --- a/client/src/lib/cache.ts +++ /dev/null @@ -1,523 +0,0 @@ -/** - * Cache abstraction layer - * - * Provides a unified caching interface with support for multiple backends: - * - In-memory cache (default) - * - Redis cache (via backend command) - * - No-op cache (disabled) - */ - -import { config } from './config'; - -/** - * Cache entry with metadata - */ -interface CacheEntry { - value: T; - expiresAt: number; -} - -/** - * Cache statistics - */ -export interface CacheStats { - hits: number; - misses: number; - items: number; - bytes: number; -} - -/** - * Cache interface that all backends implement - */ -export interface Cache { - /** - * Get a value by key - */ - get(key: string): Promise; - - /** - * Set a value with optional TTL (in seconds) - */ - set(key: string, value: T, ttlSecs?: number): Promise; - - /** - * Delete a value by key - */ - delete(key: string): Promise; - - /** - * Delete all values with keys matching the given prefix - */ - deleteByPrefix(prefix: string): Promise; - - /** - * Check if a key exists - */ - exists(key: string): Promise; - - /** - * Clear all cached values - */ - clear(): Promise; - - /** - * Get cache statistics - */ - stats(): CacheStats; -} - -/** - * In-memory cache implementation with LRU eviction - */ -class MemoryCache implements Cache { - private entries = new Map>(); - private accessOrder: string[] = []; - private hits = 0; - private misses = 0; - - constructor( - private maxItems: number = 1000, - private defaultTtlSecs: number = 300 - ) {} - - async get(key: string): Promise { - const entry = this.entries.get(key); - - if (!entry) { - this.misses++; - return null; - } - - if (Date.now() > entry.expiresAt) { - this.entries.delete(key); - this.removeFromAccessOrder(key); - this.misses++; - return null; - } - - this.hits++; - this.updateAccessOrder(key); - return entry.value as T; - } - - async set(key: string, value: T, ttlSecs?: number): Promise { - // Guard against infinite loop if maxItems is zero or negative - if (this.maxItems <= 0) { - return; - } - - const ttl = ttlSecs ?? this.defaultTtlSecs; - const expiresAt = Date.now() + ttl * 1000; - - // Evict expired entries - this.evictExpired(); - - // Evict LRU if at capacity - while (this.entries.size >= this.maxItems) { - this.evictLru(); - } - - this.entries.set(key, { value, expiresAt }); - this.updateAccessOrder(key); - } - - async delete(key: string): Promise { - const existed = this.entries.delete(key); - if (existed) { - this.removeFromAccessOrder(key); - } - return existed; - } - - async deleteByPrefix(prefix: string): Promise { - // Collect keys first to avoid mutating map during iteration - const keysToDelete: string[] = []; - for (const key of this.entries.keys()) { - if (key.startsWith(prefix)) { - keysToDelete.push(key); - } - } - - for (const key of keysToDelete) { - this.entries.delete(key); - this.removeFromAccessOrder(key); - } - - return keysToDelete.length; - } - - async exists(key: string): Promise { - const entry = this.entries.get(key); - if (!entry) return false; - - if (Date.now() > entry.expiresAt) { - this.entries.delete(key); - this.removeFromAccessOrder(key); - return false; - } - - return true; - } - - async clear(): Promise { - this.entries.clear(); - this.accessOrder = []; - this.hits = 0; - this.misses = 0; - } - - stats(): CacheStats { - let bytes = 0; - for (const [key, entry] of this.entries) { - bytes += key.length * 2; // Approximate string size - try { - bytes += JSON.stringify(entry.value).length * 2; - } catch { - // Handle non-serializable values (circular refs, etc.) - bytes += 100; // Estimate for non-serializable values - } - } - - return { - hits: this.hits, - misses: this.misses, - items: this.entries.size, - bytes, - }; - } - - private updateAccessOrder(key: string): void { - this.removeFromAccessOrder(key); - this.accessOrder.push(key); - } - - private removeFromAccessOrder(key: string): void { - const index = this.accessOrder.indexOf(key); - if (index > -1) { - this.accessOrder.splice(index, 1); - } - } - - private evictExpired(): void { - const now = Date.now(); - for (const [key, entry] of this.entries) { - if (now > entry.expiresAt) { - this.entries.delete(key); - this.removeFromAccessOrder(key); - } - } - } - - private evictLru(): void { - if (this.accessOrder.length > 0) { - const lruKey = this.accessOrder.shift(); - if (lruKey) { - this.entries.delete(lruKey); - } - } - } -} - -/** - * No-op cache implementation (when caching is disabled) - */ -class NoOpCache implements Cache { - async get(_key: string): Promise { - return null; - } - - async set(_key: string, _value: T, _ttlSecs?: number): Promise {} - - async delete(_key: string): Promise { - return false; - } - - async deleteByPrefix(_prefix: string): Promise { - return 0; - } - - async exists(_key: string): Promise { - return false; - } - - async clear(): Promise {} - - stats(): CacheStats { - return { hits: 0, misses: 0, items: 0, bytes: 0 }; - } -} - -/** - * Redis cache implementation (delegates to Tauri backend) - * - * Uses atomic fallback switching to prevent race conditions when - * multiple concurrent requests fail and try to switch to memory cache. - */ -class RedisCache implements Cache { - private memoryFallback: MemoryCache; - private usesFallback = false; - private fallbackSwitchPromise: Promise | null = null; - - constructor( - private defaultTtlSecs: number = 300, - maxItems: number = 1000 - ) { - this.memoryFallback = new MemoryCache(maxItems, defaultTtlSecs); - } - - /** - * Atomically switch to fallback mode, ensuring only one switch happens - * Note: Error details are not logged to avoid exposing sensitive connection info - */ - private async switchToFallback(): Promise { - if (this.usesFallback) return; - - // Use a promise to serialize fallback switching - if (!this.fallbackSwitchPromise) { - this.fallbackSwitchPromise = (async () => { - console.warn('Redis cache unavailable, falling back to memory cache'); - this.usesFallback = true; - })(); - } - await this.fallbackSwitchPromise; - } - - /** - * Execute a Redis operation with automatic fallback to memory cache on failure - */ - private async withFallback( - primary: () => Promise, - fallback: () => Promise - ): Promise { - if (this.usesFallback) { - return fallback(); - } - - try { - return await primary(); - } catch { - await this.switchToFallback(); - return fallback(); - } - } - - async get(key: string): Promise { - return this.withFallback( - async () => { - const { invoke } = await import('@tauri-apps/api/core'); - const result = await invoke('cache_get', { key }); - if (result === null) return null; - - try { - return JSON.parse(result) as T; - } catch { - // Treat as cache miss (corrupt value), not backend failure. - // Best-effort delete to prevent repeated parse failures. - try { - await invoke('cache_delete', { key }); - } catch { - // ignore delete failure - } - return null; - } - }, - () => this.memoryFallback.get(key) - ); - } - - async set(key: string, value: T, ttlSecs?: number): Promise { - // Pre-serialize to catch errors before triggering fallback - let serialized: string; - try { - serialized = JSON.stringify(value); - } catch { - // Non-serializable values shouldn't trigger Redis fallback, but can still be cached in memory - await this.memoryFallback.set(key, value, ttlSecs); - return; - } - - return this.withFallback( - async () => { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('cache_set', { - key, - value: serialized, - ttlSecs: ttlSecs ?? this.defaultTtlSecs, - }); - }, - () => this.memoryFallback.set(key, value, ttlSecs) - ); - } - - async delete(key: string): Promise { - return this.withFallback( - async () => { - const { invoke } = await import('@tauri-apps/api/core'); - return invoke('cache_delete', { key }); - }, - () => this.memoryFallback.delete(key) - ); - } - - async deleteByPrefix(prefix: string): Promise { - return this.withFallback( - async () => { - const { invoke } = await import('@tauri-apps/api/core'); - return invoke('cache_delete_prefix', { prefix }); - }, - () => this.memoryFallback.deleteByPrefix(prefix) - ); - } - - async exists(key: string): Promise { - return this.withFallback( - async () => { - const { invoke } = await import('@tauri-apps/api/core'); - return invoke('cache_exists', { key }); - }, - () => this.memoryFallback.exists(key) - ); - } - - async clear(): Promise { - return this.withFallback( - async () => { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('cache_clear'); - }, - () => this.memoryFallback.clear() - ); - } - - stats(): CacheStats { - // Redis stats would need a backend call - // For now, return fallback stats - return this.memoryFallback.stats(); - } -} - -/** - * Create a cache instance based on configuration - */ -function createCache(): Cache { - const cacheConfig = config.getSection('cache'); - - switch (cacheConfig.backend) { - case 'redis': - return new RedisCache(cacheConfig.defaultTtlSecs, cacheConfig.maxMemoryItems); - case 'none': - return new NoOpCache(); - case 'memory': - default: - return new MemoryCache(cacheConfig.maxMemoryItems, cacheConfig.defaultTtlSecs); - } -} - -/** Global cache instance */ -let cacheInstance: Cache | null = null; - -/** - * Get the cache instance (creates on first access) - */ -export function getCache(): Cache { - if (!cacheInstance) { - cacheInstance = createCache(); - } - return cacheInstance; -} - -/** - * Reset the cache instance (useful for testing or config changes) - */ -export function resetCache(): void { - cacheInstance = null; -} - -/** - * Cache key builder for consistent key formatting - */ -export const CacheKey = { - /** Build a key for meeting data */ - meeting: (meetingId: string) => `meeting:${meetingId}`, - - /** Build a key for meeting list */ - meetingList: () => 'meetings:list', - - /** Build a key for segments */ - segments: (meetingId: string) => `segments:${meetingId}`, - - /** Build a key for summary */ - summary: (meetingId: string) => `summary:${meetingId}`, - - /** Build a key for server info */ - serverInfo: () => 'server:info', - - /** Build a custom key with prefix */ - custom: (prefix: string, id: string) => `${prefix}:${id}`, -} as const; - -/** - * Helper to wrap an async function with caching - */ -export function withCache( - keyFn: (...args: Args) => string, - fn: (...args: Args) => Promise, - ttlSecs?: number -): (...args: Args) => Promise { - return async (...args: Args): Promise => { - const cache = getCache(); - const key = keyFn(...args); - - // Try cache first - const cached = await cache.get(key); - if (cached !== null) { - return cached; - } - - // Call the function - const result = await fn(...args); - - // Cache the result (skip null/undefined to avoid permanent misses) - if (result != null) { - await cache.set(key, result, ttlSecs); - } - - return result; - }; -} - -/** - * Decorator-style cache wrapper for class methods - */ -export function cached( - key: string | ((args: unknown[]) => string), - ttlSecs?: number -) { - return ( - _target: unknown, - _propertyKey: string, - descriptor: PropertyDescriptor - ) => { - const originalMethod = descriptor.value; - - descriptor.value = async function (...args: unknown[]) { - const cache = getCache(); - const cacheKey = typeof key === 'function' ? key(args) : key; - - const cached = await cache.get(cacheKey); - if (cached !== null) { - return cached; - } - - const result = await originalMethod.apply(this, args); - // Skip caching null/undefined to avoid permanent misses - if (result != null) { - await cache.set(cacheKey, result, ttlSecs); - } - return result; - }; - - return descriptor; - }; -} diff --git a/client/src/lib/config.ts b/client/src/lib/config.ts deleted file mode 100644 index 365b058..0000000 --- a/client/src/lib/config.ts +++ /dev/null @@ -1,368 +0,0 @@ -/** - * Application configuration - * - * Centralized configuration loaded from environment variables or localStorage. - * All configurable values should be defined here rather than hardcoded. - */ - -/** Storage key for persisted config */ -const CONFIG_STORAGE_KEY = 'noteflow:config'; - -/** - * Server connection configuration - */ -export interface ServerConfig { - /** Default server address */ - defaultAddress: string; - /** Connection timeout in milliseconds */ - connectTimeoutMs: number; - /** Request timeout in milliseconds */ - requestTimeoutMs: number; - /** Maximum retry attempts */ - maxRetries: number; -} - -/** - * Audio configuration - */ -export interface AudioConfig { - /** Default sample rate (Hz) */ - sampleRate: number; - /** Default number of channels */ - channels: number; - /** VU meter update rate (Hz) */ - vuUpdateRate: number; -} - -/** - * UI configuration - */ -export interface UIConfig { - /** Theme preference */ - theme: 'light' | 'dark' | 'system'; - /** Show VU meter during recording */ - showVuMeter: boolean; - /** Auto-scroll transcript during recording */ - autoScrollTranscript: boolean; - /** Highlight duration in ms when segment is clicked */ - highlightDurationMs: number; -} - -/** - * Trigger configuration - */ -export interface TriggerConfig { - /** Enable trigger detection */ - enabled: boolean; - /** Default snooze duration in minutes */ - snoozeDurationMinutes: number; -} - -/** - * Cache configuration - */ -export interface CacheConfig { - /** Cache backend type */ - backend: 'memory' | 'redis' | 'none'; - /** Redis URL (if using Redis) */ - redisUrl?: string; - /** Default TTL for cached items (seconds) */ - defaultTtlSecs: number; - /** Maximum memory cache size (items) */ - maxMemoryItems: number; -} - -/** - * Full application configuration - */ -export interface AppConfig { - server: ServerConfig; - audio: AudioConfig; - ui: UIConfig; - triggers: TriggerConfig; - cache: CacheConfig; -} - -/** - * Get Vite environment variable safely - */ -function getEnv(key: string): string | undefined { - try { - // Access via dynamic property to avoid TypeScript errors - const meta = import.meta as unknown as { env?: Record }; - return meta.env?.[key]; - } catch { - return undefined; - } -} - -/** - * Default configuration values - */ -const defaultConfig: AppConfig = { - server: { - defaultAddress: getEnv('VITE_SERVER_ADDRESS') ?? 'localhost:50051', - connectTimeoutMs: 5000, - requestTimeoutMs: 30000, - maxRetries: 3, - }, - audio: { - sampleRate: 16000, - channels: 1, - vuUpdateRate: 20, - }, - ui: { - theme: 'system', - showVuMeter: true, - autoScrollTranscript: true, - highlightDurationMs: 2000, - }, - triggers: { - enabled: false, - snoozeDurationMinutes: 5, - }, - cache: { - backend: 'memory', - redisUrl: getEnv('VITE_REDIS_URL'), - defaultTtlSecs: 300, - maxMemoryItems: 1000, - }, -}; - -/** - * Clamp a number to a range - */ -function clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(max, value)); -} - -/** - * Validate and sanitize server config - */ -function validateServerConfig(config: Partial | undefined, defaults: ServerConfig): ServerConfig { - return { - defaultAddress: typeof config?.defaultAddress === 'string' && config.defaultAddress.length > 0 - ? config.defaultAddress - : defaults.defaultAddress, - connectTimeoutMs: typeof config?.connectTimeoutMs === 'number' - ? clamp(config.connectTimeoutMs, 1000, 60000) - : defaults.connectTimeoutMs, - requestTimeoutMs: typeof config?.requestTimeoutMs === 'number' - ? clamp(config.requestTimeoutMs, 1000, 300000) - : defaults.requestTimeoutMs, - maxRetries: typeof config?.maxRetries === 'number' - ? clamp(Math.floor(config.maxRetries), 0, 10) - : defaults.maxRetries, - }; -} - -/** - * Validate and sanitize audio config - */ -function validateAudioConfig(config: Partial | undefined, defaults: AudioConfig): AudioConfig { - return { - sampleRate: typeof config?.sampleRate === 'number' - ? clamp(Math.floor(config.sampleRate), 8000, 48000) - : defaults.sampleRate, - channels: typeof config?.channels === 'number' - ? clamp(Math.floor(config.channels), 1, 2) - : defaults.channels, - vuUpdateRate: typeof config?.vuUpdateRate === 'number' - ? clamp(Math.floor(config.vuUpdateRate), 1, 60) - : defaults.vuUpdateRate, - }; -} - -/** - * Validate and sanitize UI config - */ -function validateUIConfig(config: Partial | undefined, defaults: UIConfig): UIConfig { - const validThemes = ['light', 'dark', 'system'] as const; - return { - theme: config?.theme && validThemes.includes(config.theme as typeof validThemes[number]) - ? config.theme - : defaults.theme, - showVuMeter: typeof config?.showVuMeter === 'boolean' - ? config.showVuMeter - : defaults.showVuMeter, - autoScrollTranscript: typeof config?.autoScrollTranscript === 'boolean' - ? config.autoScrollTranscript - : defaults.autoScrollTranscript, - highlightDurationMs: typeof config?.highlightDurationMs === 'number' - ? clamp(Math.floor(config.highlightDurationMs), 500, 10000) - : defaults.highlightDurationMs, - }; -} - -/** - * Validate and sanitize trigger config - */ -function validateTriggerConfig(config: Partial | undefined, defaults: TriggerConfig): TriggerConfig { - return { - enabled: typeof config?.enabled === 'boolean' - ? config.enabled - : defaults.enabled, - snoozeDurationMinutes: typeof config?.snoozeDurationMinutes === 'number' - ? clamp(Math.floor(config.snoozeDurationMinutes), 1, 120) - : defaults.snoozeDurationMinutes, - }; -} - -/** - * Validate and sanitize cache config - */ -function validateCacheConfig(config: Partial | undefined, defaults: CacheConfig): CacheConfig { - const validBackends = ['memory', 'redis', 'none'] as const; - return { - backend: config?.backend && validBackends.includes(config.backend as typeof validBackends[number]) - ? config.backend - : defaults.backend, - // redisUrl is intentionally not loaded from localStorage for security - redisUrl: defaults.redisUrl, - defaultTtlSecs: typeof config?.defaultTtlSecs === 'number' - ? clamp(Math.floor(config.defaultTtlSecs), 1, 86400) - : defaults.defaultTtlSecs, - maxMemoryItems: typeof config?.maxMemoryItems === 'number' - ? clamp(Math.floor(config.maxMemoryItems), 10, 100000) - : defaults.maxMemoryItems, - }; -} - -/** - * Configuration manager class - */ -class ConfigManager { - private config: AppConfig; - private listeners: Set<(config: AppConfig) => void> = new Set(); - - constructor() { - this.config = this.loadConfig(); - } - - /** - * Load configuration from localStorage, merging with defaults - * Validates all values against expected types and bounds - */ - private loadConfig(): AppConfig { - try { - const stored = localStorage.getItem(CONFIG_STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored) as Partial; - return this.validateAndMergeConfig(defaultConfig, parsed); - } - } catch { - console.warn('Failed to load config from localStorage, using defaults'); - } - return { ...defaultConfig }; - } - - /** - * Validate and merge configuration objects with type checking and bounds - */ - private validateAndMergeConfig(base: AppConfig, override: Partial): AppConfig { - return { - server: validateServerConfig(override.server, base.server), - audio: validateAudioConfig(override.audio, base.audio), - ui: validateUIConfig(override.ui, base.ui), - triggers: validateTriggerConfig(override.triggers, base.triggers), - cache: validateCacheConfig(override.cache, base.cache), - }; - } - - /** - * Simple merge for internal updates (already validated) - */ - private mergeConfig(base: AppConfig, override: Partial): AppConfig { - return this.validateAndMergeConfig(base, override); - } - - /** - * Get current configuration - */ - get(): AppConfig { - return { ...this.config }; - } - - /** - * Get a specific config section - */ - getSection(section: K): AppConfig[K] { - return { ...this.config[section] }; - } - - /** - * Update configuration - */ - set(updates: Partial): void { - this.config = this.mergeConfig(this.config, updates); - this.persist(); - this.notifyListeners(); - } - - /** - * Update a specific section (validates updates through mergeConfig) - */ - setSection( - section: K, - updates: Partial - ): void { - this.config = this.mergeConfig(this.config, { - [section]: { ...this.config[section], ...updates }, - } as Partial); - this.persist(); - this.notifyListeners(); - } - - /** - * Reset configuration to defaults - */ - reset(): void { - this.config = { ...defaultConfig }; - localStorage.removeItem(CONFIG_STORAGE_KEY); - this.notifyListeners(); - } - - /** - * Subscribe to configuration changes - */ - subscribe(listener: (config: AppConfig) => void): () => void { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - /** - * Persist configuration to localStorage - */ - private persist(): void { - try { - localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(this.config)); - } catch { - console.warn('Failed to persist config to localStorage'); - } - } - - /** - * Notify all listeners of configuration changes - */ - private notifyListeners(): void { - const config = this.get(); - this.listeners.forEach((listener) => listener(config)); - } -} - -/** Global configuration instance */ -export const config = new ConfigManager(); - -/** - * React hook for using configuration - */ -export function useConfig(): AppConfig; -export function useConfig(section: K): AppConfig[K]; -export function useConfig(section?: K) { - // This would integrate with React state management - // For now, just return the current config - if (section) { - return config.getSection(section); - } - return config.get(); -} diff --git a/client/src/lib/constants.test.ts b/client/src/lib/constants.test.ts deleted file mode 100644 index 6e4e30c..0000000 --- a/client/src/lib/constants.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - AnnotationTypes, - Defaults, - Events, - ExportFormats, - MeetingStates, - PlaybackStates, - ViewModes, -} from './constants'; - -describe('Events', () => { - it('should have uppercase event names', () => { - Object.values(Events).forEach((event) => { - expect(event).toBe(event.toUpperCase()); - }); - }); - - it('should have all required events', () => { - expect(Events.TRANSCRIPT_UPDATE).toBeDefined(); - expect(Events.AUDIO_LEVEL).toBeDefined(); - expect(Events.PLAYBACK_POSITION).toBeDefined(); - expect(Events.PLAYBACK_STATE).toBeDefined(); - expect(Events.HIGHLIGHT_CHANGE).toBeDefined(); - expect(Events.CONNECTION_CHANGE).toBeDefined(); - expect(Events.MEETING_DETECTED).toBeDefined(); - expect(Events.RECORDING_TIMER).toBeDefined(); - expect(Events.ERROR).toBeDefined(); - expect(Events.SUMMARY_PROGRESS).toBeDefined(); - expect(Events.DIARIZATION_PROGRESS).toBeDefined(); - }); - - it('should have unique event names', () => { - const values = Object.values(Events); - const uniqueValues = new Set(values); - expect(uniqueValues.size).toBe(values.length); - }); -}); - -describe('PlaybackStates', () => { - it('should have all playback states', () => { - expect(PlaybackStates.STOPPED).toBe('stopped'); - expect(PlaybackStates.PLAYING).toBe('playing'); - expect(PlaybackStates.PAUSED).toBe('paused'); - }); - - it('should have exactly 3 states', () => { - expect(Object.keys(PlaybackStates)).toHaveLength(3); - }); -}); - -describe('AnnotationTypes', () => { - it('should have all annotation types', () => { - expect(AnnotationTypes.ACTION_ITEM).toBe('action_item'); - expect(AnnotationTypes.DECISION).toBe('decision'); - expect(AnnotationTypes.NOTE).toBe('note'); - expect(AnnotationTypes.RISK).toBe('risk'); - }); -}); - -describe('ExportFormats', () => { - it('should have markdown format', () => { - expect(ExportFormats.MARKDOWN).toBe('markdown'); - }); - - it('should have html format', () => { - expect(ExportFormats.HTML).toBe('html'); - }); -}); - -describe('MeetingStates', () => { - it('should have all meeting states', () => { - expect(MeetingStates.UNSPECIFIED).toBe('unspecified'); - expect(MeetingStates.CREATED).toBe('created'); - expect(MeetingStates.RECORDING).toBe('recording'); - expect(MeetingStates.STOPPED).toBe('stopped'); - expect(MeetingStates.COMPLETED).toBe('completed'); - expect(MeetingStates.ERROR).toBe('error'); - expect(MeetingStates.STOPPING).toBe('stopping'); - }); -}); - -describe('ViewModes', () => { - it('should have all view modes', () => { - expect(ViewModes.RECORDING).toBe('recording'); - expect(ViewModes.REVIEW).toBe('review'); - expect(ViewModes.LIBRARY).toBe('library'); - }); -}); - -describe('Defaults', () => { - it('should have default server address', () => { - expect(Defaults.SERVER_ADDRESS).toBe('localhost:50051'); - }); - - it('should have default recording title prefix', () => { - expect(Defaults.RECORDING_TITLE_PREFIX).toBe('Meeting'); - }); - - it('should have snooze duration', () => { - expect(Defaults.SNOOZE_MINUTES).toBe(5); - }); -}); diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts deleted file mode 100644 index 4fa864a..0000000 --- a/client/src/lib/constants.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Application-wide constants - * - * Centralized location for all hardcoded values to ensure consistency - * between TypeScript frontend and Rust backend. - */ - -/** - * Event names emitted from backend to frontend - * Must match the event names in src-tauri/src/constants.rs - */ -export const Events = { - /** Transcript update (partial or final segment) */ - TRANSCRIPT_UPDATE: 'TRANSCRIPT_UPDATE', - /** Audio level change (normalized 0.0-1.0) */ - AUDIO_LEVEL: 'AUDIO_LEVEL', - /** Playback position change (seconds) */ - PLAYBACK_POSITION: 'PLAYBACK_POSITION', - /** Playback state change (playing/paused/stopped) */ - PLAYBACK_STATE: 'PLAYBACK_STATE', - /** Highlighted segment index change */ - HIGHLIGHT_CHANGE: 'HIGHLIGHT_CHANGE', - /** Connection state change */ - CONNECTION_CHANGE: 'CONNECTION_CHANGE', - /** Meeting detected by trigger system */ - MEETING_DETECTED: 'MEETING_DETECTED', - /** Recording timer tick (elapsed seconds) */ - RECORDING_TIMER: 'RECORDING_TIMER', - /** Error event */ - ERROR: 'ERROR', - /** Summary generation progress */ - SUMMARY_PROGRESS: 'SUMMARY_PROGRESS', - /** Diarization job progress */ - DIARIZATION_PROGRESS: 'DIARIZATION_PROGRESS', -} as const; - -/** Type for event names */ -export type EventName = (typeof Events)[keyof typeof Events]; - -/** - * Playback states - */ -export const PlaybackStates = { - STOPPED: 'stopped', - PLAYING: 'playing', - PAUSED: 'paused', -} as const; - -/** Type for playback states */ -export type PlaybackStateName = (typeof PlaybackStates)[keyof typeof PlaybackStates]; - -/** - * Annotation types - */ -export const AnnotationTypes = { - ACTION_ITEM: 'action_item', - DECISION: 'decision', - NOTE: 'note', - RISK: 'risk', -} as const; - -/** Type for annotation types */ -export type AnnotationTypeName = (typeof AnnotationTypes)[keyof typeof AnnotationTypes]; - -/** - * Export formats - */ -export const ExportFormats = { - MARKDOWN: 'markdown', - HTML: 'html', -} as const; - -/** Type for export formats */ -export type ExportFormatName = (typeof ExportFormats)[keyof typeof ExportFormats]; - -/** - * Meeting states - */ -export const MeetingStates = { - UNSPECIFIED: 'unspecified', - CREATED: 'created', - RECORDING: 'recording', - STOPPED: 'stopped', - COMPLETED: 'completed', - ERROR: 'error', - STOPPING: 'stopping', -} as const; - -/** Type for meeting states */ -export type MeetingStateName = (typeof MeetingStates)[keyof typeof MeetingStates]; - -/** - * View modes for the main application - */ -export const ViewModes = { - RECORDING: 'recording', - REVIEW: 'review', - LIBRARY: 'library', -} as const; - -/** Type for view modes */ -export type ViewModeName = (typeof ViewModes)[keyof typeof ViewModes]; - -/** - * Default values - */ -export const Defaults = { - /** Default server address */ - SERVER_ADDRESS: 'localhost:50051', - /** Default recording title prefix */ - RECORDING_TITLE_PREFIX: 'Meeting', - /** Trigger snooze duration in minutes */ - SNOOZE_MINUTES: 5, -} as const; diff --git a/client/src/lib/tauri.ts b/client/src/lib/tauri.ts deleted file mode 100644 index 5c3c368..0000000 --- a/client/src/lib/tauri.ts +++ /dev/null @@ -1,380 +0,0 @@ -/** - * Tauri API facade - Service-based API for Tauri commands - * - * This module provides typed service classes for all Tauri IPC communication. - * Services are organized by domain for better maintainability and type safety. - */ - -import { invoke } from '@tauri-apps/api/core'; -import type { - AnnotationInfo, - AppStatus, - AudioDeviceInfo, - DiarizationResult, - ExportResult, - MeetingDetails, - MeetingInfo, - PlaybackInfo, - RenameSpeakerResult, - ServerInfo, - SummaryInfo, - TriggerStatus, - UserPreferences, -} from '@/types'; -import { CacheKey, getCache } from './cache'; - -/** Cache TTL constants (in seconds) */ -const CACHE_TTL = { - SERVER_INFO: 60, - MEETING_LIST: 30, - MEETING_DETAILS: 120, - SUMMARY: 300, -} as const; - -/** - * Connection service - manages server connection lifecycle - */ -class ConnectionService { - async connect(address: string): Promise { - // Clear cached server info on new connection - const cache = getCache(); - await cache.delete(CacheKey.serverInfo()); - return invoke('connect', { address }); - } - - async disconnect(): Promise { - // Clear cached server info on disconnect - const cache = getCache(); - await cache.delete(CacheKey.serverInfo()); - return invoke('disconnect'); - } - - async getServerInfo(): Promise { - const cache = getCache(); - const key = CacheKey.serverInfo(); - - // Try cache first - const cached = await cache.get(key); - if (cached) return cached; - - // Fetch from backend - const info = await invoke('get_server_info'); - if (info) { - await cache.set(key, info, CACHE_TTL.SERVER_INFO); - } - return info; - } - - getStatus(): Promise { - return invoke('get_status'); - } -} - -/** - * Recording service - manages recording sessions - */ -class RecordingService { - start(title: string): Promise { - return invoke('start_recording', { title }); - } - - async stop(): Promise { - const result = await invoke('stop_recording'); - // Invalidate all meeting list cache variants after recording stops - const cache = getCache(); - await cache.deleteByPrefix('meetings:list'); - return result; - } -} - -/** - * Meeting service - manages meetings CRUD operations - */ -class MeetingService { - async list( - states?: string[], - limit?: number, - offset?: number, - sortDesc?: boolean - ): Promise<[MeetingInfo[], number]> { - // Meeting list can be cached with a short TTL for quick navigation - const cache = getCache(); - const key = CacheKey.custom('meetings:list', `${states?.join(',') ?? 'all'}:${limit ?? 0}:${offset ?? 0}:${sortDesc ?? false}`); - - const cached = await cache.get<[MeetingInfo[], number]>(key); - if (cached) return cached; - - const result = await invoke<[MeetingInfo[], number]>('list_meetings', { states, limit, offset, sort_desc: sortDesc }); - await cache.set(key, result, CACHE_TTL.MEETING_LIST); - return result; - } - - async get( - meetingId: string, - includeSegments: boolean, - includeSummary: boolean - ): Promise { - const cache = getCache(); - const key = CacheKey.custom('meeting', `${meetingId}:${includeSegments}:${includeSummary}`); - - const cached = await cache.get(key); - if (cached) return cached; - - const result = await invoke('get_meeting', { meeting_id: meetingId, include_segments: includeSegments, include_summary: includeSummary }); - await cache.set(key, result, CACHE_TTL.MEETING_DETAILS); - return result; - } - - async delete(meetingId: string): Promise { - // Perform the delete operation first - const result = await invoke('delete_meeting', { meeting_id: meetingId }); - - // Only invalidate caches after successful deletion - if (result) { - const cache = getCache(); - // Remove meeting-specific cached variants (avoid prefix-collisions across ids) - await cache.delete(CacheKey.meeting(meetingId)); - await cache.deleteByPrefix(`meeting:${meetingId}:`); - await cache.delete(CacheKey.segments(meetingId)); - await cache.delete(CacheKey.summary(meetingId)); - // Invalidate all meeting list cache variants (parameterized by states/limit/offset/sort) - await cache.deleteByPrefix('meetings:list'); - } - - return result; - } - - select(meetingId: string): Promise { - return invoke('select_meeting', { meeting_id: meetingId }); - } - - /** - * Invalidate meeting-related caches without affecting unrelated entries (e.g. serverInfo) - * @param meetingId - If provided, invalidates caches for this specific meeting - */ - async invalidateCache(meetingId?: string): Promise { - const cache = getCache(); - - // Invalidate per-meeting caches if a specific meetingId is provided - if (meetingId) { - await cache.delete(CacheKey.meeting(meetingId)); - await cache.delete(CacheKey.segments(meetingId)); - await cache.delete(CacheKey.summary(meetingId)); - } - - // Invalidate all meeting list cache variants without touching unrelated caches - await cache.deleteByPrefix('meetings:list'); - await cache.deleteByPrefix('meeting:'); - } -} - -/** - * Annotation service - manages meeting annotations - */ -class AnnotationService { - add( - meetingId: string, - annotationType: string, - text: string, - startTime: number, - endTime: number, - segmentIds?: number[] - ): Promise { - return invoke('add_annotation', { - meeting_id: meetingId, - annotation_type: annotationType, - text, - start_time: startTime, - end_time: endTime, - segment_ids: segmentIds, - }); - } - - get(annotationId: string): Promise { - return invoke('get_annotation', { annotation_id: annotationId }); - } - - list(meetingId: string, startTime?: number, endTime?: number): Promise { - return invoke('list_annotations', { meeting_id: meetingId, start_time: startTime, end_time: endTime }); - } - - update( - annotationId: string, - annotationType?: string, - text?: string, - startTime?: number, - endTime?: number, - segmentIds?: number[] - ): Promise { - return invoke('update_annotation', { - annotation_id: annotationId, - annotation_type: annotationType, - text, - start_time: startTime, - end_time: endTime, - segment_ids: segmentIds, - }); - } - - delete(annotationId: string): Promise { - return invoke('delete_annotation', { annotation_id: annotationId }); - } -} - -/** - * Summary service - manages AI-generated summaries - */ -class SummaryService { - generate(meetingId: string, forceRegenerate: boolean): Promise { - return invoke('generate_summary', { meeting_id: meetingId, force_regenerate: forceRegenerate }); - } -} - -/** - * Export service - manages transcript export - */ -class ExportService { - transcript(meetingId: string, format: string): Promise { - return invoke('export_transcript', { meeting_id: meetingId, format }); - } - - saveFile(content: string, defaultName: string, extension: string): Promise { - return invoke('save_export_file', { content, default_name: defaultName, extension }); - } -} - -/** - * Diarization service - manages speaker diarization - */ -class DiarizationService { - refine(meetingId: string, numSpeakers?: number): Promise { - return invoke('refine_speaker_diarization', { meeting_id: meetingId, num_speakers: numSpeakers }); - } - - getJobStatus(jobId: string): Promise { - return invoke('get_diarization_job_status', { job_id: jobId }); - } - - renameSpeaker( - meetingId: string, - oldSpeakerId: string, - newSpeakerName: string - ): Promise { - return invoke('rename_speaker', { meeting_id: meetingId, old_speaker_id: oldSpeakerId, new_speaker_name: newSpeakerName }); - } -} - -/** - * Playback service - manages audio playback controls - */ -class PlaybackService { - play(): Promise { - return invoke('play'); - } - - pause(): Promise { - return invoke('pause'); - } - - stop(): Promise { - return invoke('stop'); - } - - seek(position: number): Promise { - return invoke('seek', { position }); - } - - getState(): Promise { - return invoke('get_playback_state'); - } -} - -/** - * Trigger service - manages auto-start triggers - */ -class TriggerService { - setEnabled(enabled: boolean): Promise { - return invoke('set_trigger_enabled', { enabled }); - } - - snooze(minutes?: number): Promise { - return invoke('snooze_triggers', { minutes }); - } - - resetSnooze(): Promise { - return invoke('reset_snooze'); - } - - getStatus(): Promise { - return invoke('get_trigger_status'); - } - - dismiss(): Promise { - return invoke('dismiss_trigger'); - } - - accept(title?: string): Promise { - return invoke('accept_trigger', { title }); - } -} - -/** - * Audio service - manages audio device configuration - */ -class AudioService { - listDevices(): Promise { - return invoke('list_audio_devices'); - } - - selectDevice(deviceId?: number): Promise { - const payload: Record = {}; - if (deviceId !== undefined) payload.device_id = deviceId; - return invoke('select_audio_device', payload); - } - - getCurrentDevice(): Promise { - return invoke('get_current_device'); - } -} - -/** - * Preferences service - manages user preferences - */ -class PreferencesService { - get(): Promise { - return invoke('get_preferences'); - } - - save(preferences: UserPreferences): Promise { - return invoke('save_preferences', { preferences }); - } -} - -/** - * NoteFlow API facade - unified interface for all Tauri commands - * - * Usage: - * ```ts - * import { api } from '@/lib/tauri'; - * - * await api.connection.connect('localhost:50051'); - * await api.recording.start('My Meeting'); - * await api.playback.play(); - * ``` - */ -class NoteFlowApi { - readonly connection = new ConnectionService(); - readonly recording = new RecordingService(); - readonly meetings = new MeetingService(); - readonly annotations = new AnnotationService(); - readonly summary = new SummaryService(); - readonly export = new ExportService(); - readonly diarization = new DiarizationService(); - readonly playback = new PlaybackService(); - readonly triggers = new TriggerService(); - readonly audio = new AudioService(); - readonly preferences = new PreferencesService(); -} - -/** Singleton API instance */ -export const api = new NoteFlowApi(); diff --git a/client/src/lib/utils.test.ts b/client/src/lib/utils.test.ts deleted file mode 100644 index e2b6a44..0000000 --- a/client/src/lib/utils.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { cn, formatDateTime, formatTime, getSpeakerColor } from './utils'; - -describe('cn (className merger)', () => { - it('should merge class names', () => { - expect(cn('foo', 'bar')).toBe('foo bar'); - }); - - it('should handle conditional classes', () => { - expect(cn('base', true && 'active')).toBe('base active'); - expect(cn('base', false && 'hidden')).toBe('base'); - }); - - it('should handle undefined and null values', () => { - expect(cn('base', undefined, null, 'end')).toBe('base end'); - }); - - it('should merge Tailwind classes correctly', () => { - // Later class should override earlier class for same property - expect(cn('p-2', 'p-4')).toBe('p-4'); - expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500'); - }); - - it('should handle object notation', () => { - expect(cn({ foo: true, bar: false })).toBe('foo'); - }); - - it('should handle array notation', () => { - expect(cn(['foo', 'bar'])).toBe('foo bar'); - }); -}); - -describe('formatTime', () => { - it('should format seconds under a minute', () => { - expect(formatTime(0)).toBe('00:00'); - expect(formatTime(30)).toBe('00:30'); - expect(formatTime(59)).toBe('00:59'); - }); - - it('should format minutes and seconds', () => { - expect(formatTime(60)).toBe('01:00'); - expect(formatTime(90)).toBe('01:30'); - expect(formatTime(300)).toBe('05:00'); - expect(formatTime(599)).toBe('09:59'); - }); - - it('should format hours, minutes and seconds', () => { - expect(formatTime(3600)).toBe('01:00:00'); - expect(formatTime(3661)).toBe('01:01:01'); - expect(formatTime(7200)).toBe('02:00:00'); - expect(formatTime(3723)).toBe('01:02:03'); - }); - - it('should handle decimal seconds by flooring', () => { - expect(formatTime(59.9)).toBe('00:59'); - expect(formatTime(90.5)).toBe('01:30'); - }); - - it('should pad single digits', () => { - expect(formatTime(5)).toBe('00:05'); - expect(formatTime(65)).toBe('01:05'); - expect(formatTime(3605)).toBe('01:00:05'); - }); -}); - -describe('formatDateTime', () => { - it('should format unix timestamp to locale string', () => { - // Note: This test depends on locale, so we just verify it returns a string - const result = formatDateTime(1704067200); // 2024-01-01 00:00:00 UTC - expect(typeof result).toBe('string'); - expect(result.length).toBeGreaterThan(0); - }); - - it('should handle different timestamps', () => { - const result1 = formatDateTime(0); // 1970-01-01 - const result2 = formatDateTime(1609459200); // 2021-01-01 - expect(result1).not.toBe(result2); - }); -}); - -describe('getSpeakerColor', () => { - it('should return a consistent color for the same speaker ID', () => { - const color1 = getSpeakerColor('speaker-1'); - const color2 = getSpeakerColor('speaker-1'); - expect(color1).toBe(color2); - }); - - it('should return different colors for different speakers', () => { - // Not guaranteed to be different due to hash collisions, but should be in most cases - // This is a probabilistic test - const colors = [ - getSpeakerColor('alice'), - getSpeakerColor('bob'), - getSpeakerColor('charlie'), - getSpeakerColor('diana'), - getSpeakerColor('edward'), - ]; - const uniqueColors = new Set(colors); - expect(uniqueColors.size).toBeGreaterThanOrEqual(3); // At least 3 unique colors for 5 speakers - }); - - it('should return HSL color format', () => { - const color = getSpeakerColor('test-speaker'); - expect(color).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/); - }); - - it('should handle empty string', () => { - const color = getSpeakerColor(''); - expect(color).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/); - }); - - it('should handle Unicode characters', () => { - const color = getSpeakerColor('日本語スピーカー'); - expect(color).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/); - }); -}); diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts deleted file mode 100644 index b151e4e..0000000 --- a/client/src/lib/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -/** - * Format seconds to MM:SS or HH:MM:SS - */ -export function formatTime(seconds: number): string { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = Math.floor(seconds % 60); - - if (h > 0) { - return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; - } - return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; -} - -/** - * Format timestamp to locale string - */ -export function formatDateTime(timestamp: number): string { - return new Date(timestamp * 1000).toLocaleString(); -} - -/** - * Generate a consistent color for a speaker ID - */ -export function getSpeakerColor(speakerId: string): string { - const colors = [ - 'hsl(210, 100%, 60%)', // Blue - 'hsl(150, 60%, 50%)', // Green - 'hsl(45, 100%, 50%)', // Yellow - 'hsl(280, 60%, 60%)', // Purple - 'hsl(30, 100%, 50%)', // Orange - 'hsl(180, 60%, 50%)', // Cyan - 'hsl(340, 80%, 60%)', // Pink - 'hsl(120, 60%, 40%)', // Dark Green - ]; - - // Hash the speaker ID to get a consistent color index - let hash = 0; - for (let i = 0; i < speakerId.length; i++) { - hash = (hash << 5) - hash + speakerId.charCodeAt(i); - hash |= 0; - } - - return colors[Math.abs(hash) % colors.length]; -} diff --git a/client/src/main.tsx b/client/src/main.tsx deleted file mode 100644 index 5e4e74f..0000000 --- a/client/src/main.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { App } from './App'; -import './index.css'; - -const rootEl = document.getElementById('root'); -if (!rootEl) { - throw new Error('Root element "#root" not found'); -} - -ReactDOM.createRoot(rootEl).render( - - - -); diff --git a/client/src/store/index.ts b/client/src/store/index.ts deleted file mode 100644 index 741c2bf..0000000 --- a/client/src/store/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Zustand store - composed from domain slices - * - * This module creates a unified store from individual slices, - * providing better organization and maintainability. - */ - -import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; - -import { - type ConnectionSlice, - createConnectionSlice, - createMeetingsSlice, - createPlaybackSlice, - createRecordingSlice, - createTranscriptSlice, - createTriggersSlice, - createViewSlice, - type MeetingsSlice, - type PlaybackSlice, - type RecordingSlice, - type TranscriptSlice, - type TriggersSlice, - type ViewSlice, -} from './slices'; - -// Re-export ViewMode type for convenience -export type { ViewMode } from './slices'; - -/** - * Combined app state - intersection of all slices - */ -export type AppState = ConnectionSlice & - RecordingSlice & - TranscriptSlice & - PlaybackSlice & - MeetingsSlice & - TriggersSlice & - ViewSlice; - -/** - * Unified store with all slices composed - */ -export const useStore = create()( - devtools( - persist( - immer((...args) => ({ - ...createConnectionSlice(...args), - ...createRecordingSlice(...args), - ...createTranscriptSlice(...args), - ...createPlaybackSlice(...args), - ...createMeetingsSlice(...args), - ...createTriggersSlice(...args), - ...createViewSlice(...args), - })), - { - name: 'noteflow-storage', - partialize: (state) => ({ - serverAddress: state.serverAddress, - triggerEnabled: state.triggerEnabled, - }), - } - ) - ) -); diff --git a/client/src/store/slices/connection.test.ts b/client/src/store/slices/connection.test.ts deleted file mode 100644 index 35b13cd..0000000 --- a/client/src/store/slices/connection.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import type { ServerInfo } from '@/types'; -import { type ConnectionSlice, createConnectionSlice } from './connection'; - -const createTestStore = () => - create()(immer((...a) => ({ ...createConnectionSlice(...a) }))); - -describe('ConnectionSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = createTestStore(); - }); - - describe('initial state', () => { - it('should have default values', () => { - const state = store.getState(); - expect(state.connected).toBe(false); - expect(state.serverAddress).toBe('localhost:50051'); - expect(state.serverInfo).toBeNull(); - }); - }); - - describe('setConnectionState', () => { - it('should update connection to connected state with server info', () => { - const serverInfo: ServerInfo = { - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - }; - - store.getState().setConnectionState(true, '192.168.1.1:50051', serverInfo); - - const state = store.getState(); - expect(state.connected).toBe(true); - expect(state.serverAddress).toBe('192.168.1.1:50051'); - expect(state.serverInfo).toEqual(serverInfo); - }); - - it('should update connection to disconnected state', () => { - // First connect - const serverInfo: ServerInfo = { - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - }; - store.getState().setConnectionState(true, 'localhost:50051', serverInfo); - - // Then disconnect - store.getState().setConnectionState(false, 'localhost:50051'); - - const state = store.getState(); - expect(state.connected).toBe(false); - expect(state.serverInfo).toBeNull(); - }); - - it('should clear server info when not provided', () => { - const serverInfo: ServerInfo = { - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - }; - store.getState().setConnectionState(true, 'localhost:50051', serverInfo); - - store.getState().setConnectionState(false, 'localhost:50051'); - - expect(store.getState().serverInfo).toBeNull(); - }); - - it('should handle address changes', () => { - store.getState().setConnectionState(true, 'server1:50051'); - expect(store.getState().serverAddress).toBe('server1:50051'); - - store.getState().setConnectionState(true, 'server2:50051'); - expect(store.getState().serverAddress).toBe('server2:50051'); - }); - }); -}); diff --git a/client/src/store/slices/connection.ts b/client/src/store/slices/connection.ts deleted file mode 100644 index b80875e..0000000 --- a/client/src/store/slices/connection.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Connection state slice - manages server connection - */ - -import type { StateCreator } from 'zustand'; -import type { ServerInfo } from '@/types'; - -export interface ConnectionSlice { - connected: boolean; - serverAddress: string; - serverInfo: ServerInfo | null; - setConnectionState: (connected: boolean, address: string, info?: ServerInfo) => void; -} - -export const createConnectionSlice: StateCreator = ( - set -) => ({ - connected: false, - serverAddress: 'localhost:50051', - serverInfo: null, - setConnectionState: (connected, address, info) => - set((state) => { - state.connected = connected; - state.serverAddress = address; - state.serverInfo = info ?? null; - }), -}); diff --git a/client/src/store/slices/index.ts b/client/src/store/slices/index.ts deleted file mode 100644 index 1d6e99a..0000000 --- a/client/src/store/slices/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Store slices barrel export - */ - -export { type ConnectionSlice, createConnectionSlice } from './connection'; -export { createMeetingsSlice, type MeetingsSlice } from './meetings'; -export { createPlaybackSlice, type PlaybackSlice } from './playback'; -export { createRecordingSlice, type RecordingSlice } from './recording'; -export { createTranscriptSlice, type TranscriptSlice } from './transcript'; -export { createTriggersSlice, type TriggersSlice } from './triggers'; -export { createViewSlice, type ViewMode, type ViewSlice } from './view'; diff --git a/client/src/store/slices/meetings.test.ts b/client/src/store/slices/meetings.test.ts deleted file mode 100644 index 1f1705a..0000000 --- a/client/src/store/slices/meetings.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import { createMockActionItem, createMockKeyPoint } from '@/test/mocks/tauri'; -import type { AnnotationInfo, MeetingInfo, SummaryInfo } from '@/types'; -import { createMeetingsSlice, type MeetingsSlice } from './meetings'; - -const createTestStore = () => - create()(immer((...a) => ({ ...createMeetingsSlice(...a) }))); - -const mockMeetings: MeetingInfo[] = [ - { - id: 'meeting-1', - title: 'Meeting 1', - state: 'completed', - created_at: Date.now() - 86400000, - started_at: Date.now() - 86400000, - ended_at: Date.now() - 82800000, - duration_seconds: 3600, - segment_count: 10, - }, - { - id: 'meeting-2', - title: 'Meeting 2', - state: 'recording', - created_at: Date.now(), - started_at: Date.now(), - ended_at: 0, - duration_seconds: 1800, - segment_count: 5, - }, -]; - -const mockAnnotation: AnnotationInfo = { - id: 'annotation-1', - meeting_id: 'meeting-1', - annotation_type: 'note', - text: 'Test annotation', - start_time: 0, - end_time: 300, - segment_ids: [1], - created_at: Date.now() - 86100000, -}; - -const mockSummary: SummaryInfo = { - meeting_id: 'meeting-1', - executive_summary: 'Summary content', - key_points: [createMockKeyPoint({ text: 'Point 1' }), createMockKeyPoint({ text: 'Point 2' })], - action_items: [createMockActionItem({ text: 'Action 1' })], - generated_at: Date.now() - 82800000, - model_version: '1.0.0', -}; - -describe('MeetingsSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = createTestStore(); - }); - - describe('initial state', () => { - it('should have default values', () => { - const state = store.getState(); - expect(state.meetings).toEqual([]); - expect(state.selectedMeeting).toBeNull(); - expect(state.annotations).toEqual([]); - expect(state.summary).toBeNull(); - expect(state.summaryLoading).toBe(false); - expect(state.summaryError).toBeNull(); - }); - }); - - describe('setMeetings', () => { - it('should set meetings list', () => { - store.getState().setMeetings(mockMeetings); - expect(store.getState().meetings).toEqual(mockMeetings); - }); - - it('should replace existing meetings', () => { - store.getState().setMeetings(mockMeetings); - store.getState().setMeetings([mockMeetings[0]]); - expect(store.getState().meetings).toHaveLength(1); - }); - - it('should handle empty array', () => { - store.getState().setMeetings(mockMeetings); - store.getState().setMeetings([]); - expect(store.getState().meetings).toEqual([]); - }); - }); - - describe('setSelectedMeeting', () => { - it('should select a meeting', () => { - store.getState().setSelectedMeeting(mockMeetings[0]); - expect(store.getState().selectedMeeting).toEqual(mockMeetings[0]); - }); - - it('should clear selection when set to null', () => { - store.getState().setSelectedMeeting(mockMeetings[0]); - store.getState().setSelectedMeeting(null); - expect(store.getState().selectedMeeting).toBeNull(); - }); - }); - - describe('annotations', () => { - it('should set annotations', () => { - const annotations = [mockAnnotation]; - store.getState().setAnnotations(annotations); - expect(store.getState().annotations).toEqual(annotations); - }); - - it('should add an annotation', () => { - store.getState().addAnnotation(mockAnnotation); - expect(store.getState().annotations).toContainEqual(mockAnnotation); - }); - - it('should add multiple annotations', () => { - const annotation2: AnnotationInfo = { ...mockAnnotation, id: 'annotation-2' }; - store.getState().addAnnotation(mockAnnotation); - store.getState().addAnnotation(annotation2); - expect(store.getState().annotations).toHaveLength(2); - }); - - it('should remove an annotation by id', () => { - store.getState().addAnnotation(mockAnnotation); - store.getState().removeAnnotation('annotation-1'); - expect(store.getState().annotations).toEqual([]); - }); - - it('should only remove matching annotation', () => { - const annotation2: AnnotationInfo = { ...mockAnnotation, id: 'annotation-2' }; - store.getState().addAnnotation(mockAnnotation); - store.getState().addAnnotation(annotation2); - store.getState().removeAnnotation('annotation-1'); - expect(store.getState().annotations).toHaveLength(1); - expect(store.getState().annotations[0].id).toBe('annotation-2'); - }); - }); - - describe('summary', () => { - it('should set summary', () => { - store.getState().setSummary(mockSummary); - expect(store.getState().summary).toEqual(mockSummary); - }); - - it('should clear summary when set to null', () => { - store.getState().setSummary(mockSummary); - store.getState().setSummary(null); - expect(store.getState().summary).toBeNull(); - }); - - it('should set summary loading state', () => { - store.getState().setSummaryLoading(true); - expect(store.getState().summaryLoading).toBe(true); - }); - - it('should set summary error', () => { - store.getState().setSummaryError('Failed to generate summary'); - expect(store.getState().summaryError).toBe('Failed to generate summary'); - }); - - it('should clear summary error', () => { - store.getState().setSummaryError('Error'); - store.getState().setSummaryError(null); - expect(store.getState().summaryError).toBeNull(); - }); - }); - - describe('summary workflow', () => { - it('should handle loading, success workflow', () => { - // Start loading - store.getState().setSummaryLoading(true); - store.getState().setSummaryError(null); - expect(store.getState().summaryLoading).toBe(true); - - // Success - store.getState().setSummary(mockSummary); - store.getState().setSummaryLoading(false); - - expect(store.getState().summaryLoading).toBe(false); - expect(store.getState().summary).toEqual(mockSummary); - expect(store.getState().summaryError).toBeNull(); - }); - - it('should handle loading, error workflow', () => { - // Start loading - store.getState().setSummaryLoading(true); - - // Error - store.getState().setSummaryError('Network error'); - store.getState().setSummaryLoading(false); - - expect(store.getState().summaryLoading).toBe(false); - expect(store.getState().summary).toBeNull(); - expect(store.getState().summaryError).toBe('Network error'); - }); - }); -}); diff --git a/client/src/store/slices/meetings.ts b/client/src/store/slices/meetings.ts deleted file mode 100644 index 070e9cd..0000000 --- a/client/src/store/slices/meetings.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Meetings state slice - manages meeting library - */ - -import type { StateCreator } from 'zustand'; -import type { AnnotationInfo, MeetingInfo, SummaryInfo } from '@/types'; - -export interface MeetingsSlice { - meetings: MeetingInfo[]; - selectedMeeting: MeetingInfo | null; - annotations: AnnotationInfo[]; - summary: SummaryInfo | null; - summaryLoading: boolean; - summaryError: string | null; - setMeetings: (meetings: MeetingInfo[]) => void; - setSelectedMeeting: (meeting: MeetingInfo | null) => void; - setAnnotations: (annotations: AnnotationInfo[]) => void; - addAnnotation: (annotation: AnnotationInfo) => void; - removeAnnotation: (id: string) => void; - setSummary: (summary: SummaryInfo | null) => void; - setSummaryLoading: (loading: boolean) => void; - setSummaryError: (error: string | null) => void; -} - -export const createMeetingsSlice: StateCreator = ( - set -) => ({ - meetings: [], - selectedMeeting: null, - annotations: [], - summary: null, - summaryLoading: false, - summaryError: null, - setMeetings: (meetings) => - set((state) => { - state.meetings = meetings; - }), - setSelectedMeeting: (meeting) => - set((state) => { - state.selectedMeeting = meeting; - }), - setAnnotations: (annotations) => - set((state) => { - state.annotations = annotations; - }), - addAnnotation: (annotation) => - set((state) => { - state.annotations.push(annotation); - }), - removeAnnotation: (id) => - set((state) => { - state.annotations = state.annotations.filter((a) => a.id !== id); - }), - setSummary: (summary) => - set((state) => { - state.summary = summary; - }), - setSummaryLoading: (loading) => - set((state) => { - state.summaryLoading = loading; - }), - setSummaryError: (error) => - set((state) => { - state.summaryError = error; - }), -}); diff --git a/client/src/store/slices/playback.test.ts b/client/src/store/slices/playback.test.ts deleted file mode 100644 index 46247b7..0000000 --- a/client/src/store/slices/playback.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import { createPlaybackSlice, type PlaybackSlice } from './playback'; - -const createTestStore = () => - create()(immer((...a) => ({ ...createPlaybackSlice(...a) }))); - -describe('PlaybackSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = createTestStore(); - }); - - describe('initial state', () => { - it('should have default values', () => { - const state = store.getState(); - expect(state.playbackState).toBe('stopped'); - expect(state.playbackPosition).toBe(0); - expect(state.playbackDuration).toBe(0); - }); - }); - - describe('setPlaybackState', () => { - it('should set to playing', () => { - store.getState().setPlaybackState('playing'); - expect(store.getState().playbackState).toBe('playing'); - }); - - it('should set to paused', () => { - store.getState().setPlaybackState('paused'); - expect(store.getState().playbackState).toBe('paused'); - }); - - it('should set to stopped', () => { - store.getState().setPlaybackState('playing'); - store.getState().setPlaybackState('stopped'); - expect(store.getState().playbackState).toBe('stopped'); - }); - }); - - describe('setPlaybackPosition', () => { - it('should update position', () => { - store.getState().setPlaybackPosition(30.5); - expect(store.getState().playbackPosition).toBe(30.5); - }); - - it('should handle zero', () => { - store.getState().setPlaybackPosition(100); - store.getState().setPlaybackPosition(0); - expect(store.getState().playbackPosition).toBe(0); - }); - }); - - describe('setPlaybackDuration', () => { - it('should update duration', () => { - store.getState().setPlaybackDuration(120.0); - expect(store.getState().playbackDuration).toBe(120.0); - }); - }); - - describe('playback workflow', () => { - it('should handle play, pause, stop workflow', () => { - // Load media - store.getState().setPlaybackDuration(180.0); - - // Play - store.getState().setPlaybackState('playing'); - store.getState().setPlaybackPosition(10.0); - - expect(store.getState().playbackState).toBe('playing'); - expect(store.getState().playbackPosition).toBe(10.0); - - // Pause - store.getState().setPlaybackState('paused'); - expect(store.getState().playbackState).toBe('paused'); - expect(store.getState().playbackPosition).toBe(10.0); - - // Resume - store.getState().setPlaybackState('playing'); - store.getState().setPlaybackPosition(20.0); - - // Stop - store.getState().setPlaybackState('stopped'); - store.getState().setPlaybackPosition(0); - - expect(store.getState().playbackState).toBe('stopped'); - expect(store.getState().playbackPosition).toBe(0); - }); - }); -}); diff --git a/client/src/store/slices/playback.ts b/client/src/store/slices/playback.ts deleted file mode 100644 index 50dac04..0000000 --- a/client/src/store/slices/playback.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Playback state slice - manages audio playback controls - */ - -import type { StateCreator } from 'zustand'; -import type { PlaybackState } from '@/types'; - -export interface PlaybackSlice { - playbackState: PlaybackState; - playbackPosition: number; - playbackDuration: number; - setPlaybackState: (state: PlaybackState) => void; - setPlaybackPosition: (position: number) => void; - setPlaybackDuration: (duration: number) => void; -} - -export const createPlaybackSlice: StateCreator = ( - set -) => ({ - playbackState: 'stopped', - playbackPosition: 0, - playbackDuration: 0, - setPlaybackState: (playbackState) => - set((state) => { - state.playbackState = playbackState; - }), - setPlaybackPosition: (position) => - set((state) => { - state.playbackPosition = position; - }), - setPlaybackDuration: (duration) => - set((state) => { - state.playbackDuration = duration; - }), -}); diff --git a/client/src/store/slices/recording.test.ts b/client/src/store/slices/recording.test.ts deleted file mode 100644 index c906940..0000000 --- a/client/src/store/slices/recording.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import type { MeetingInfo } from '@/types'; -import { createRecordingSlice, type RecordingSlice } from './recording'; - -const createTestStore = () => - create()(immer((...a) => ({ ...createRecordingSlice(...a) }))); - -const mockMeeting: MeetingInfo = { - id: 'meeting-123', - title: 'Test Meeting', - state: 'recording', - created_at: Date.now(), - started_at: Date.now(), - ended_at: 0, - duration_seconds: 0, - segment_count: 0, -}; - -describe('RecordingSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = createTestStore(); - }); - - describe('initial state', () => { - it('should have default values', () => { - const state = store.getState(); - expect(state.recording).toBe(false); - expect(state.currentMeeting).toBeNull(); - expect(state.elapsedSeconds).toBe(0); - expect(state.audioLevel).toBe(0); - }); - }); - - describe('setRecording', () => { - it('should set recording to true', () => { - store.getState().setRecording(true); - expect(store.getState().recording).toBe(true); - }); - - it('should set recording to false', () => { - store.getState().setRecording(true); - store.getState().setRecording(false); - expect(store.getState().recording).toBe(false); - }); - }); - - describe('setCurrentMeeting', () => { - it('should set current meeting', () => { - store.getState().setCurrentMeeting(mockMeeting); - expect(store.getState().currentMeeting).toEqual(mockMeeting); - }); - - it('should clear current meeting when set to null', () => { - store.getState().setCurrentMeeting(mockMeeting); - store.getState().setCurrentMeeting(null); - expect(store.getState().currentMeeting).toBeNull(); - }); - }); - - describe('setElapsedSeconds', () => { - it('should update elapsed seconds', () => { - store.getState().setElapsedSeconds(60); - expect(store.getState().elapsedSeconds).toBe(60); - }); - - it('should handle zero', () => { - store.getState().setElapsedSeconds(100); - store.getState().setElapsedSeconds(0); - expect(store.getState().elapsedSeconds).toBe(0); - }); - - it('should handle large values', () => { - store.getState().setElapsedSeconds(3600); - expect(store.getState().elapsedSeconds).toBe(3600); - }); - }); - - describe('setAudioLevel', () => { - it('should update audio level', () => { - store.getState().setAudioLevel(0.75); - expect(store.getState().audioLevel).toBe(0.75); - }); - - it('should handle min value', () => { - store.getState().setAudioLevel(0); - expect(store.getState().audioLevel).toBe(0); - }); - - it('should handle max value', () => { - store.getState().setAudioLevel(1); - expect(store.getState().audioLevel).toBe(1); - }); - }); - - describe('recording workflow', () => { - it('should handle complete recording workflow', () => { - // Start recording - store.getState().setCurrentMeeting(mockMeeting); - store.getState().setRecording(true); - - expect(store.getState().recording).toBe(true); - expect(store.getState().currentMeeting).toEqual(mockMeeting); - - // Update elapsed time - store.getState().setElapsedSeconds(30); - expect(store.getState().elapsedSeconds).toBe(30); - - // Update audio levels - store.getState().setAudioLevel(0.5); - expect(store.getState().audioLevel).toBe(0.5); - - // Stop recording - store.getState().setRecording(false); - store.getState().setElapsedSeconds(0); - - expect(store.getState().recording).toBe(false); - expect(store.getState().elapsedSeconds).toBe(0); - }); - }); -}); diff --git a/client/src/store/slices/recording.ts b/client/src/store/slices/recording.ts deleted file mode 100644 index d0896aa..0000000 --- a/client/src/store/slices/recording.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Recording state slice - manages recording sessions and audio - */ - -import type { StateCreator } from 'zustand'; -import type { MeetingInfo } from '@/types'; - -export interface RecordingSlice { - recording: boolean; - currentMeeting: MeetingInfo | null; - elapsedSeconds: number; - audioLevel: number; - setRecording: (recording: boolean) => void; - setCurrentMeeting: (meeting: MeetingInfo | null) => void; - setElapsedSeconds: (seconds: number) => void; - setAudioLevel: (level: number) => void; -} - -export const createRecordingSlice: StateCreator = ( - set -) => ({ - recording: false, - currentMeeting: null, - elapsedSeconds: 0, - audioLevel: 0, - setRecording: (recording) => - set((state) => { - state.recording = recording; - }), - setCurrentMeeting: (meeting) => - set((state) => { - state.currentMeeting = meeting; - }), - setElapsedSeconds: (seconds) => - set((state) => { - state.elapsedSeconds = seconds; - }), - setAudioLevel: (level) => - set((state) => { - state.audioLevel = level; - }), -}); diff --git a/client/src/store/slices/transcript.test.ts b/client/src/store/slices/transcript.test.ts deleted file mode 100644 index 7115dc9..0000000 --- a/client/src/store/slices/transcript.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import type { Segment } from '@/types'; -import { createTranscriptSlice, type TranscriptSlice } from './transcript'; - -const createTestStore = () => - create()(immer((...a) => ({ ...createTranscriptSlice(...a) }))); - -const createMockSegment = (overrides?: Partial): Segment => ({ - segment_id: 1, - text: 'Hello, this is a test segment.', - start_time: 0.0, - end_time: 5.0, - language: 'en', - speaker_id: 'speaker-1', - speaker_confidence: 0.9, - words: [], - ...overrides, -}); - -describe('TranscriptSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = createTestStore(); - }); - - describe('initial state', () => { - it('should have default values', () => { - const state = store.getState(); - expect(state.segments).toEqual([]); - expect(state.partialText).toBe(''); - expect(state.highlightedSegment).toBeNull(); - }); - }); - - describe('addSegment', () => { - it('should add a segment', () => { - const segment = createMockSegment(); - store.getState().addSegment(segment); - expect(store.getState().segments).toContainEqual(segment); - }); - - it('should add multiple segments in order', () => { - const segment1 = createMockSegment({ segment_id: 1, start_time: 0 }); - const segment2 = createMockSegment({ segment_id: 2, start_time: 5 }); - const segment3 = createMockSegment({ segment_id: 3, start_time: 10 }); - - store.getState().addSegment(segment1); - store.getState().addSegment(segment2); - store.getState().addSegment(segment3); - - const segments = store.getState().segments; - expect(segments).toHaveLength(3); - expect(segments[0].segment_id).toBe(1); - expect(segments[1].segment_id).toBe(2); - expect(segments[2].segment_id).toBe(3); - }); - - it('should preserve existing segments', () => { - const segment1 = createMockSegment({ segment_id: 1 }); - store.getState().addSegment(segment1); - - const segment2 = createMockSegment({ segment_id: 2 }); - store.getState().addSegment(segment2); - - expect(store.getState().segments).toHaveLength(2); - }); - }); - - describe('setPartialText', () => { - it('should update partial text', () => { - store.getState().setPartialText('Partial...'); - expect(store.getState().partialText).toBe('Partial...'); - }); - - it('should replace existing partial text', () => { - store.getState().setPartialText('First'); - store.getState().setPartialText('Second'); - expect(store.getState().partialText).toBe('Second'); - }); - - it('should handle empty string', () => { - store.getState().setPartialText('Something'); - store.getState().setPartialText(''); - expect(store.getState().partialText).toBe(''); - }); - }); - - describe('setHighlightedSegment', () => { - it('should set highlighted segment index', () => { - store.getState().setHighlightedSegment(5); - expect(store.getState().highlightedSegment).toBe(5); - }); - - it('should clear highlighted segment', () => { - store.getState().setHighlightedSegment(5); - store.getState().setHighlightedSegment(null); - expect(store.getState().highlightedSegment).toBeNull(); - }); - - it('should handle zero index', () => { - store.getState().setHighlightedSegment(0); - expect(store.getState().highlightedSegment).toBe(0); - }); - }); - - describe('clearTranscript', () => { - it('should clear all transcript state', () => { - // Set up state - store.getState().addSegment(createMockSegment()); - store.getState().addSegment(createMockSegment({ segment_id: 2 })); - store.getState().setPartialText('Partial...'); - store.getState().setHighlightedSegment(0); - - // Clear - store.getState().clearTranscript(); - - const state = store.getState(); - expect(state.segments).toEqual([]); - expect(state.partialText).toBe(''); - expect(state.highlightedSegment).toBeNull(); - }); - }); - - describe('live transcription workflow', () => { - it('should handle live transcription with partial updates', () => { - // Partial text appears first - store.getState().setPartialText('Hello'); - expect(store.getState().partialText).toBe('Hello'); - - // More partial text - store.getState().setPartialText('Hello world'); - expect(store.getState().partialText).toBe('Hello world'); - - // Finalized segment arrives, partial clears - store.getState().addSegment(createMockSegment({ text: 'Hello world!' })); - store.getState().setPartialText(''); - - expect(store.getState().segments).toHaveLength(1); - expect(store.getState().partialText).toBe(''); - - // Next partial starts - store.getState().setPartialText('How are'); - expect(store.getState().partialText).toBe('How are'); - }); - }); -}); diff --git a/client/src/store/slices/transcript.ts b/client/src/store/slices/transcript.ts deleted file mode 100644 index b52b758..0000000 --- a/client/src/store/slices/transcript.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Transcript state slice - manages transcript segments - */ - -import type { StateCreator } from 'zustand'; -import type { Segment } from '@/types'; - -export interface TranscriptSlice { - segments: Segment[]; - partialText: string; - highlightedSegment: number | null; - addSegment: (segment: Segment) => void; - setPartialText: (text: string) => void; - setHighlightedSegment: (index: number | null) => void; - clearTranscript: () => void; -} - -export const createTranscriptSlice: StateCreator = ( - set -) => ({ - segments: [], - partialText: '', - highlightedSegment: null, - addSegment: (segment) => - set((state) => { - state.segments.push(segment); - }), - setPartialText: (text) => - set((state) => { - state.partialText = text; - }), - setHighlightedSegment: (index) => - set((state) => { - state.highlightedSegment = index; - }), - clearTranscript: () => - set((state) => { - state.segments = []; - state.partialText = ''; - state.highlightedSegment = null; - }), -}); diff --git a/client/src/store/slices/triggers.test.ts b/client/src/store/slices/triggers.test.ts deleted file mode 100644 index 4aec9e6..0000000 --- a/client/src/store/slices/triggers.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import type { TriggerDecision } from '@/types'; -import { createTriggersSlice, type TriggersSlice } from './triggers'; - -const createTestStore = () => - create()(immer((...a) => ({ ...createTriggersSlice(...a) }))); - -const mockTriggerDecision: TriggerDecision = { - action: 'notify', - confidence: 0.85, - signals: [], - timestamp: Date.now(), - detected_app: 'Zoom', -}; - -describe('TriggersSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = createTestStore(); - }); - - describe('initial state', () => { - it('should have default values', () => { - const state = store.getState(); - expect(state.triggerEnabled).toBe(true); - expect(state.triggerPending).toBe(false); - expect(state.triggerDecision).toBeNull(); - }); - }); - - describe('setTriggerEnabled', () => { - it('should enable triggers', () => { - store.getState().setTriggerEnabled(false); - store.getState().setTriggerEnabled(true); - expect(store.getState().triggerEnabled).toBe(true); - }); - - it('should disable triggers', () => { - store.getState().setTriggerEnabled(false); - expect(store.getState().triggerEnabled).toBe(false); - }); - }); - - describe('setTriggerPending', () => { - it('should set pending to true', () => { - store.getState().setTriggerPending(true); - expect(store.getState().triggerPending).toBe(true); - }); - - it('should set pending to false', () => { - store.getState().setTriggerPending(true); - store.getState().setTriggerPending(false); - expect(store.getState().triggerPending).toBe(false); - }); - }); - - describe('setTriggerDecision', () => { - it('should set trigger decision', () => { - store.getState().setTriggerDecision(mockTriggerDecision); - expect(store.getState().triggerDecision).toEqual(mockTriggerDecision); - }); - - it('should clear trigger decision', () => { - store.getState().setTriggerDecision(mockTriggerDecision); - store.getState().setTriggerDecision(null); - expect(store.getState().triggerDecision).toBeNull(); - }); - - it('should handle different action types', () => { - const autoStartDecision: TriggerDecision = { - ...mockTriggerDecision, - action: 'auto_start', - confidence: 0.95, - }; - store.getState().setTriggerDecision(autoStartDecision); - expect(store.getState().triggerDecision?.action).toBe('auto_start'); - }); - }); - - describe('trigger workflow', () => { - it('should handle trigger detection and acceptance', () => { - // Trigger detected - store.getState().setTriggerDecision(mockTriggerDecision); - store.getState().setTriggerPending(true); - - expect(store.getState().triggerPending).toBe(true); - expect(store.getState().triggerDecision).toEqual(mockTriggerDecision); - - // User accepts trigger - store.getState().setTriggerPending(false); - store.getState().setTriggerDecision(null); - - expect(store.getState().triggerPending).toBe(false); - expect(store.getState().triggerDecision).toBeNull(); - }); - - it('should handle trigger dismissal', () => { - // Trigger detected - store.getState().setTriggerDecision(mockTriggerDecision); - store.getState().setTriggerPending(true); - - // User dismisses - store.getState().setTriggerPending(false); - store.getState().setTriggerDecision(null); - - expect(store.getState().triggerPending).toBe(false); - expect(store.getState().triggerDecision).toBeNull(); - }); - - it('should handle trigger disable/enable cycle', () => { - // Disable triggers - store.getState().setTriggerEnabled(false); - expect(store.getState().triggerEnabled).toBe(false); - - // Re-enable - store.getState().setTriggerEnabled(true); - expect(store.getState().triggerEnabled).toBe(true); - }); - }); -}); diff --git a/client/src/store/slices/triggers.ts b/client/src/store/slices/triggers.ts deleted file mode 100644 index ebff07c..0000000 --- a/client/src/store/slices/triggers.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Triggers state slice - manages auto-start triggers - */ - -import type { StateCreator } from 'zustand'; -import type { TriggerDecision } from '@/types'; - -export interface TriggersSlice { - triggerEnabled: boolean; - triggerPending: boolean; - triggerDecision: TriggerDecision | null; - setTriggerEnabled: (enabled: boolean) => void; - setTriggerPending: (pending: boolean) => void; - setTriggerDecision: (decision: TriggerDecision | null) => void; -} - -export const createTriggersSlice: StateCreator = ( - set -) => ({ - triggerEnabled: true, - triggerPending: false, - triggerDecision: null, - setTriggerEnabled: (enabled) => - set((state) => { - state.triggerEnabled = enabled; - }), - setTriggerPending: (pending) => - set((state) => { - state.triggerPending = pending; - }), - setTriggerDecision: (decision) => - set((state) => { - state.triggerDecision = decision; - }), -}); diff --git a/client/src/store/slices/view.ts b/client/src/store/slices/view.ts deleted file mode 100644 index f3c2a6e..0000000 --- a/client/src/store/slices/view.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * View state slice - manages UI navigation - */ - -import type { StateCreator } from 'zustand'; - -export type ViewMode = 'recording' | 'library' | 'settings'; - -export interface ViewSlice { - viewMode: ViewMode; - setViewMode: (mode: ViewMode) => void; -} - -export const createViewSlice: StateCreator = (set) => ({ - viewMode: 'recording' as ViewMode, - setViewMode: (mode) => - set((state) => { - state.viewMode = mode; - }), -}); diff --git a/client/src/test/mocks/tauri.ts b/client/src/test/mocks/tauri.ts deleted file mode 100644 index d21a179..0000000 --- a/client/src/test/mocks/tauri.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { vi } from 'vitest'; -import type { - ActionItem, - AudioDeviceInfo, - KeyPoint, - MeetingDetails, - MeetingInfo, - MeetingState, - PlaybackInfo, - PlaybackState, - Segment, - ServerInfo, - SummaryInfo, - TriggerAction, - TriggerDecision, -} from '@/types'; - -// Factory functions for test data -export const createMockMeeting = (overrides?: Partial): MeetingInfo => ({ - id: 'meeting-123', - title: 'Test Meeting', - state: 'recording' as MeetingState, - created_at: Date.now(), - started_at: Date.now(), - ended_at: 0, - duration_seconds: 1800, - segment_count: 5, - ...overrides, -}); - -export const createMockMeetingDetails = (overrides?: Partial): MeetingDetails => ({ - meeting: createMockMeeting(), - segments: [], - summary: null, - ...overrides, -}); - -export const createMockSegment = (overrides?: Partial): Segment => ({ - segment_id: 1, - text: 'This is a test segment.', - start_time: 0.0, - end_time: 5.0, - language: 'en', - speaker_id: 'speaker-1', - speaker_confidence: 0.95, - words: [], - ...overrides, -}); - -export const createMockKeyPoint = (overrides?: Partial): KeyPoint => ({ - text: 'Key point text', - segment_ids: [1, 2], - start_time: 0, - end_time: 60, - ...overrides, -}); - -export const createMockActionItem = (overrides?: Partial): ActionItem => ({ - text: 'Action item text', - assignee: '', - due_date: null, - priority: 1, - segment_ids: [1], - ...overrides, -}); - -export const createMockSummary = (overrides?: Partial): SummaryInfo => ({ - meeting_id: 'meeting-123', - executive_summary: 'Test summary content', - key_points: [createMockKeyPoint()], - action_items: [createMockActionItem()], - generated_at: Date.now(), - model_version: '1.0.0', - ...overrides, -}); - -export const createMockServerInfo = (overrides?: Partial): ServerInfo => ({ - version: '1.0.0', - asr_model: 'whisper-large-v3', - asr_ready: true, - uptime_seconds: 3600, - active_meetings: 0, - diarization_enabled: true, - diarization_ready: true, - ...overrides, -}); - -export const createMockPlaybackInfo = (overrides?: Partial): PlaybackInfo => ({ - state: 'stopped' as PlaybackState, - position: 0.0, - duration: 0.0, - highlighted_segment: null, - ...overrides, -}); - -export const createMockTriggerDecision = (overrides?: Partial): TriggerDecision => ({ - action: 'notify' as TriggerAction, - confidence: 0.8, - signals: [], - timestamp: Date.now(), - detected_app: 'Zoom', - ...overrides, -}); - -export const createMockAudioDevice = (overrides?: Partial): AudioDeviceInfo => ({ - id: 0, - name: 'Default Microphone', - is_default: true, - sample_rate: 44100, - channels: 1, - ...overrides, -}); - -// Mock invoke responses -export type InvokeHandler = (cmd: string, args?: Record) => unknown; - -export const createMockInvoke = (handlers: Record) => { - return vi.fn(async (cmd: string, args?: Record) => { - const handler = handlers[cmd]; - if (typeof handler === 'function') { - return handler(cmd, args); - } - if (handler !== undefined) { - return handler; - } - throw new Error(`Unhandled invoke command: ${cmd}`); - }); -}; - -// Default mock handlers for common commands -export const defaultMockHandlers: Record = { - connect: createMockServerInfo(), - disconnect: undefined, - get_status: { connected: true, address: 'localhost:50051' }, - start_recording: createMockMeeting({ state: 'recording' }), - stop_recording: createMockMeeting({ state: 'completed' }), - list_meetings: [[createMockMeeting()], 1], - get_meeting: createMockMeetingDetails(), - delete_meeting: true, - get_playback_state: createMockPlaybackInfo(), - list_audio_devices: [createMockAudioDevice()], - get_trigger_decision: null, - cache_get: null, - cache_set: undefined, - cache_delete: true, -}; diff --git a/client/src/test/setup.ts b/client/src/test/setup.ts deleted file mode 100644 index a54679f..0000000 --- a/client/src/test/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { cleanup } from '@testing-library/react'; -import { afterEach, beforeAll, vi } from 'vitest'; -import '@testing-library/jest-dom/vitest'; - -// Mock Tauri API -vi.mock('@tauri-apps/api/core', () => ({ - invoke: vi.fn(), -})); - -vi.mock('@tauri-apps/api/event', () => ({ - listen: vi.fn(() => Promise.resolve(() => {})), - emit: vi.fn(() => Promise.resolve()), -})); - -// Clean up after each test -afterEach(() => { - cleanup(); - vi.clearAllMocks(); -}); - -// Suppress console errors during tests unless debugging -beforeAll(() => { - if (!process.env.DEBUG_TESTS) { - vi.spyOn(console, 'error').mockImplementation(() => {}); - } -}); diff --git a/client/src/test/utils.tsx b/client/src/test/utils.tsx deleted file mode 100644 index 7b3c5f6..0000000 --- a/client/src/test/utils.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { type RenderOptions, render } from '@testing-library/react'; -import type React from 'react'; -import type { ReactElement } from 'react'; - -// Custom render function with providers -const AllProviders = ({ children }: { children: React.ReactNode }) => { - return <>{children}; -}; - -const customRender = ( - ui: ReactElement, - options?: Omit -) => render(ui, { wrapper: AllProviders, ...options }); - -// Re-export everything -export * from '@testing-library/react'; -export { customRender as render }; - -// Wait utilities -export const waitForNextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); - -export const waitFor = async ( - callback: () => void | Promise, - { timeout = 1000, interval = 50 } = {} -) => { - const start = Date.now(); - while (Date.now() - start < timeout) { - try { - await callback(); - return; - } catch { - await new Promise((resolve) => setTimeout(resolve, interval)); - } - } - await callback(); // Final attempt, let it throw -}; - -// Mock store utilities -export const createMockStore = (initialState: T) => { - let state = { ...initialState }; - const listeners = new Set<() => void>(); - - return { - getState: () => state, - setState: (partial: Partial | ((state: T) => Partial)) => { - const nextState = typeof partial === 'function' ? partial(state) : partial; - state = { ...state, ...nextState }; - listeners.forEach((listener) => listener()); - }, - subscribe: (listener: () => void) => { - listeners.add(listener); - return () => listeners.delete(listener); - }, - destroy: () => listeners.clear(), - }; -}; - -// Async act helper -export const actAsync = async (callback: () => Promise) => { - await callback(); - await waitForNextTick(); -}; diff --git a/client/src/types/annotation.ts b/client/src/types/annotation.ts deleted file mode 100644 index b3cacc8..0000000 --- a/client/src/types/annotation.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Annotation type enum -export type AnnotationType = 'action_item' | 'decision' | 'note' | 'risk'; - -// Annotation info -export interface AnnotationInfo { - id: string; - meeting_id: string; - annotation_type: AnnotationType; - text: string; - start_time: number; - end_time: number; - segment_ids: number[]; - created_at: number; -} - -// Annotation creation request -export interface CreateAnnotationRequest { - meeting_id: string; - annotation_type: AnnotationType; - text: string; - start_time: number; - end_time: number; - segment_ids?: number[]; -} - -// Annotation update request -export interface UpdateAnnotationRequest { - annotation_id: string; - annotation_type?: AnnotationType; - text?: string; - start_time?: number; - end_time?: number; - segment_ids?: number[]; -} diff --git a/client/src/types/audio.ts b/client/src/types/audio.ts deleted file mode 100644 index daa1b7e..0000000 --- a/client/src/types/audio.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Playback state -export type PlaybackState = 'stopped' | 'playing' | 'paused'; - -// Audio device info -export interface AudioDeviceInfo { - id: number; - name: string; - channels: number; - sample_rate: number; - is_default: boolean; -} - -// Playback info -export interface PlaybackInfo { - state: PlaybackState; - position: number; - duration: number; - highlighted_segment: number | null; -} diff --git a/client/src/types/events.ts b/client/src/types/events.ts deleted file mode 100644 index 45ea727..0000000 --- a/client/src/types/events.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Error event -export interface ErrorEvent { - code: string; - message: string; - recoverable: boolean; -} - -// Summary progress -export interface SummaryProgress { - meeting_id: string; - status: 'started' | 'generating' | 'completed' | 'failed'; - progress: number; - error: string | null; -} - -// Diarization progress -export interface DiarizationProgress { - job_id: string; - meeting_id: string; - status: 'queued' | 'running' | 'completed' | 'failed'; - segments_updated: number; - error: string | null; -} - -// Job status -export type JobStatus = 'unspecified' | 'queued' | 'running' | 'completed' | 'failed'; - -// Diarization result -export interface DiarizationResult { - job_id: string; - status: JobStatus; - segments_updated: number; - speaker_ids: string[]; - error_message: string; -} - -// Speaker rename result -export interface RenameSpeakerResult { - segments_updated: number; - success: boolean; -} - -// Export format -export type ExportFormat = 'markdown' | 'html'; - -// Export result -export interface ExportResult { - content: string; - format_name: string; - file_extension: string; -} diff --git a/client/src/types/index.ts b/client/src/types/index.ts deleted file mode 100644 index d518772..0000000 --- a/client/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Re-export all types - -export * from './annotation'; -export * from './audio'; -export * from './events'; -export * from './meeting'; -export * from './preferences'; -export * from './server'; -export * from './trigger'; diff --git a/client/src/types/meeting.ts b/client/src/types/meeting.ts deleted file mode 100644 index 41d825c..0000000 --- a/client/src/types/meeting.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Meeting state enum (mirrors Rust/Proto) -export type MeetingState = - | 'unspecified' - | 'created' - | 'recording' - | 'stopping' - | 'stopped' - | 'completed' - | 'error'; - -// Basic meeting info -export interface MeetingInfo { - id: string; - title: string; - state: MeetingState; - created_at: number; - started_at: number; - ended_at: number; - duration_seconds: number; - segment_count: number; -} - -// Meeting with full details -export interface MeetingDetails { - meeting: MeetingInfo; - segments: Segment[]; - summary: SummaryInfo | null; -} - -// Transcript segment -export interface Segment { - segment_id: number; - text: string; - start_time: number; - end_time: number; - language: string; - speaker_id: string; - speaker_confidence: number; - words: WordTiming[]; -} - -// Word-level timing -export interface WordTiming { - word: string; - start_time: number; - end_time: number; - probability: number; -} - -// Transcript update from streaming -export type UpdateType = 'unspecified' | 'partial' | 'final' | 'vad_start' | 'vad_end'; - -export interface TranscriptUpdate { - update_type: UpdateType; - partial_text: string; - segment: Segment | null; - server_timestamp: number; -} - -// Summary info -export interface SummaryInfo { - meeting_id: string; - executive_summary: string; - key_points: KeyPoint[]; - action_items: ActionItem[]; - generated_at: number; - model_version: string; -} - -// Key point from summary -export interface KeyPoint { - text: string; - segment_ids: number[]; - start_time: number; - end_time: number; -} - -// Action item from summary -export interface ActionItem { - text: string; - assignee: string; - due_date: number | null; - priority: 0 | 1 | 2 | 3; - segment_ids: number[]; -} diff --git a/client/src/types/preferences.ts b/client/src/types/preferences.ts deleted file mode 100644 index 4e94580..0000000 --- a/client/src/types/preferences.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Preferences types for user settings - */ - -/** Summarization provider options */ -export type SummarizationProvider = 'none' | 'cloud' | 'ollama'; - -/** User preferences */ -export interface UserPreferences { - serverUrl: string; - dataDirectory: string; - encryptionEnabled: boolean; - autoStartEnabled: boolean; - triggerConfidenceThreshold: number; - summarizationProvider: SummarizationProvider; - cloudApiKey: string; - ollamaUrl: string; -} diff --git a/client/src/types/server.ts b/client/src/types/server.ts deleted file mode 100644 index 9670f82..0000000 --- a/client/src/types/server.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PlaybackState } from './audio'; - -// Server info -export interface ServerInfo { - version: string; - asr_model: string; - asr_ready: boolean; - uptime_seconds: number; - active_meetings: number; - diarization_enabled: boolean; - diarization_ready: boolean; -} - -// Connection event -export interface ConnectionEvent { - connected: boolean; - server_address: string; - error: string | null; -} - -// App status snapshot -export interface AppStatus { - connected: boolean; - recording: boolean; - server_address: string; - current_meeting_id: string | null; - elapsed_seconds: number; - playback_state: PlaybackState; - playback_position: number; - playback_duration: number; - segment_count: number; - annotation_count: number; - trigger_enabled: boolean; - trigger_pending: boolean; - summary_loading: boolean; - has_summary: boolean; -} diff --git a/client/src/types/trigger.ts b/client/src/types/trigger.ts deleted file mode 100644 index 5e7a7e5..0000000 --- a/client/src/types/trigger.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Trigger source -export type TriggerSource = 'audio_activity' | 'foreground_app' | 'calendar'; - -// Trigger action -export type TriggerAction = 'ignore' | 'notify' | 'auto_start'; - -// Trigger signal -export interface TriggerSignal { - source: TriggerSource; - weight: number; - app_name: string | null; - timestamp: number; -} - -// Trigger decision -export interface TriggerDecision { - action: TriggerAction; - confidence: number; - signals: TriggerSignal[]; - timestamp: number; - detected_app: string | null; -} - -// Trigger status -export interface TriggerStatus { - enabled: boolean; - pending: boolean; - decision: TriggerDecision | null; - snoozed: boolean; - snooze_remaining: number; -} diff --git a/client/tailwind.config.js b/client/tailwind.config.js deleted file mode 100644 index 9c7c65c..0000000 --- a/client/tailwind.config.js +++ /dev/null @@ -1,67 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ['class'], - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - theme: { - 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))', - }, - recording: { - DEFAULT: 'hsl(0 84% 60%)', - pulse: 'hsl(0 84% 50%)', - }, - speaker: { - 0: 'hsl(210 100% 60%)', - 1: 'hsl(150 60% 50%)', - 2: 'hsl(45 100% 50%)', - 3: 'hsl(280 60% 60%)', - 4: 'hsl(30 100% 50%)', - }, - }, - animation: { - 'pulse-recording': 'pulse-recording 1.5s ease-in-out infinite', - 'fade-in': 'fade-in 0.2s ease-out', - 'slide-up': 'slide-up 0.3s ease-out', - }, - keyframes: { - 'pulse-recording': { - '0%, 100%': { opacity: '1' }, - '50%': { opacity: '0.5' }, - }, - 'fade-in': { - from: { opacity: '0' }, - to: { opacity: '1' }, - }, - 'slide-up': { - from: { transform: 'translateY(10px)', opacity: '0' }, - to: { transform: 'translateY(0)', opacity: '1' }, - }, - }, - }, - }, - plugins: [require('tailwindcss-animate')], -}; diff --git a/client/tsconfig.json b/client/tsconfig.json deleted file mode 100644 index ffbfd1f..0000000 --- a/client/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - /* Paths */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json deleted file mode 100644 index 97ede7e..0000000 --- a/client/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] -} diff --git a/client/vite.config.ts b/client/vite.config.ts deleted file mode 100644 index 5be7ca1..0000000 --- a/client/vite.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -export default defineConfig({ - plugins: [react()], - clearScreen: false, - server: { - port: 1420, - strictPort: true, - watch: { - ignored: ['**/src-tauri/**'], - }, - }, - envPrefix: ['VITE_', 'TAURI_'], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - build: { - target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13', - minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, - sourcemap: !!process.env.TAURI_DEBUG, - }, -}); diff --git a/client/vitest.config.ts b/client/vitest.config.ts deleted file mode 100644 index cfacc98..0000000 --- a/client/vitest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineConfig } from 'vitest/config'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -export default defineConfig({ - plugins: [react()], - test: { - globals: true, - environment: 'jsdom', - setupFiles: ['./src/test/setup.ts'], - include: ['src/**/*.{test,spec}.{ts,tsx}'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], - include: ['src/**/*.{ts,tsx}'], - exclude: [ - 'src/**/*.d.ts', - 'src/**/*.test.{ts,tsx}', - 'src/**/*.spec.{ts,tsx}', - 'src/test/**/*', - 'src/main.tsx', - ], - }, - testTimeout: 10000, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, -}); diff --git a/noteflow-api-spec.json b/noteflow-api-spec.json new file mode 100644 index 0000000..8236da2 --- /dev/null +++ b/noteflow-api-spec.json @@ -0,0 +1,537 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "info": { + "title": "NoteFlow API", + "version": "1.0.0", + "description": "Intelligent meeting notetaker API - real-time audio transcription, meeting management, and AI-powered summarization", + "protocol": "gRPC", + "package": "noteflow" + }, + "service": { + "name": "NoteFlowService", + "description": "Main service providing real-time ASR streaming and meeting management" + }, + "endpoints": { + "StreamTranscription": { + "type": "bidirectional_streaming", + "description": "Bidirectional streaming: client sends audio chunks, server returns transcripts in real-time", + "request": { + "type": "stream", + "message": "AudioChunk" + }, + "response": { + "type": "stream", + "message": "TranscriptUpdate" + } + }, + "CreateMeeting": { + "type": "unary", + "description": "Create a new meeting session", + "request": "CreateMeetingRequest", + "response": "Meeting" + }, + "StopMeeting": { + "type": "unary", + "description": "Stop an active meeting recording", + "request": "StopMeetingRequest", + "response": "Meeting" + }, + "ListMeetings": { + "type": "unary", + "description": "List meetings with optional filtering and pagination", + "request": "ListMeetingsRequest", + "response": "ListMeetingsResponse" + }, + "GetMeeting": { + "type": "unary", + "description": "Get a specific meeting by ID with optional transcript and summary", + "request": "GetMeetingRequest", + "response": "Meeting" + }, + "DeleteMeeting": { + "type": "unary", + "description": "Delete a meeting and all associated data", + "request": "DeleteMeetingRequest", + "response": "DeleteMeetingResponse" + }, + "GenerateSummary": { + "type": "unary", + "description": "Generate an AI-powered summary for a meeting", + "request": "GenerateSummaryRequest", + "response": "Summary" + }, + "AddAnnotation": { + "type": "unary", + "description": "Add a user annotation to a meeting (action item, decision, note, or risk)", + "request": "AddAnnotationRequest", + "response": "Annotation" + }, + "GetAnnotation": { + "type": "unary", + "description": "Get a specific annotation by ID", + "request": "GetAnnotationRequest", + "response": "Annotation" + }, + "ListAnnotations": { + "type": "unary", + "description": "List all annotations for a meeting with optional time range filter", + "request": "ListAnnotationsRequest", + "response": "ListAnnotationsResponse" + }, + "UpdateAnnotation": { + "type": "unary", + "description": "Update an existing annotation", + "request": "UpdateAnnotationRequest", + "response": "Annotation" + }, + "DeleteAnnotation": { + "type": "unary", + "description": "Delete an annotation", + "request": "DeleteAnnotationRequest", + "response": "DeleteAnnotationResponse" + }, + "ExportTranscript": { + "type": "unary", + "description": "Export meeting transcript to Markdown or HTML format", + "request": "ExportTranscriptRequest", + "response": "ExportTranscriptResponse" + }, + "RefineSpeakerDiarization": { + "type": "unary", + "description": "Run offline speaker diarization to improve speaker labels (background job)", + "request": "RefineSpeakerDiarizationRequest", + "response": "RefineSpeakerDiarizationResponse" + }, + "RenameSpeaker": { + "type": "unary", + "description": "Rename a speaker ID to a human-readable name", + "request": "RenameSpeakerRequest", + "response": "RenameSpeakerResponse" + }, + "GetDiarizationJobStatus": { + "type": "unary", + "description": "Check status of a background diarization job", + "request": "GetDiarizationJobStatusRequest", + "response": "DiarizationJobStatus" + }, + "GetServerInfo": { + "type": "unary", + "description": "Get server health and capabilities information", + "request": "ServerInfoRequest", + "response": "ServerInfo" + } + }, + "enums": { + "UpdateType": { + "description": "Type of transcript update", + "values": { + "UPDATE_TYPE_UNSPECIFIED": { "value": 0, "description": "Default/unspecified" }, + "UPDATE_TYPE_PARTIAL": { "value": 1, "description": "Tentative transcript, may change" }, + "UPDATE_TYPE_FINAL": { "value": 2, "description": "Confirmed segment" }, + "UPDATE_TYPE_VAD_START": { "value": 3, "description": "Voice activity started" }, + "UPDATE_TYPE_VAD_END": { "value": 4, "description": "Voice activity ended" } + } + }, + "MeetingState": { + "description": "Current state of a meeting", + "values": { + "MEETING_STATE_UNSPECIFIED": { "value": 0, "description": "Default/unspecified" }, + "MEETING_STATE_CREATED": { "value": 1, "description": "Created but not started" }, + "MEETING_STATE_RECORDING": { "value": 2, "description": "Actively recording audio" }, + "MEETING_STATE_STOPPED": { "value": 3, "description": "Recording stopped, processing may continue" }, + "MEETING_STATE_COMPLETED": { "value": 4, "description": "All processing complete" }, + "MEETING_STATE_ERROR": { "value": 5, "description": "Error occurred" } + } + }, + "SortOrder": { + "description": "Sort order for listing meetings", + "values": { + "SORT_ORDER_UNSPECIFIED": { "value": 0, "description": "Default (newest first)" }, + "SORT_ORDER_CREATED_DESC": { "value": 1, "description": "Newest first" }, + "SORT_ORDER_CREATED_ASC": { "value": 2, "description": "Oldest first" } + } + }, + "Priority": { + "description": "Priority level for action items", + "values": { + "PRIORITY_UNSPECIFIED": { "value": 0, "description": "Default/unspecified" }, + "PRIORITY_LOW": { "value": 1, "description": "Low priority" }, + "PRIORITY_MEDIUM": { "value": 2, "description": "Medium priority" }, + "PRIORITY_HIGH": { "value": 3, "description": "High priority" } + } + }, + "AnnotationType": { + "description": "Type of user annotation", + "values": { + "ANNOTATION_TYPE_UNSPECIFIED": { "value": 0, "description": "Default/unspecified" }, + "ANNOTATION_TYPE_ACTION_ITEM": { "value": 1, "description": "Action item to be done" }, + "ANNOTATION_TYPE_DECISION": { "value": 2, "description": "Decision made" }, + "ANNOTATION_TYPE_NOTE": { "value": 3, "description": "General note" }, + "ANNOTATION_TYPE_RISK": { "value": 4, "description": "Risk or concern" } + } + }, + "ExportFormat": { + "description": "Transcript export format", + "values": { + "EXPORT_FORMAT_UNSPECIFIED": { "value": 0, "description": "Default/unspecified" }, + "EXPORT_FORMAT_MARKDOWN": { "value": 1, "description": "Markdown format" }, + "EXPORT_FORMAT_HTML": { "value": 2, "description": "HTML format" } + } + }, + "JobStatus": { + "description": "Background job status", + "values": { + "JOB_STATUS_UNSPECIFIED": { "value": 0, "description": "Default/unspecified" }, + "JOB_STATUS_QUEUED": { "value": 1, "description": "Job is queued" }, + "JOB_STATUS_RUNNING": { "value": 2, "description": "Job is running" }, + "JOB_STATUS_COMPLETED": { "value": 3, "description": "Job completed successfully" }, + "JOB_STATUS_FAILED": { "value": 4, "description": "Job failed" } + } + } + }, + "messages": { + "AudioChunk": { + "description": "Audio data chunk for streaming transcription", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID this audio belongs to" }, + "audio_data": { "type": "bytes", "required": true, "description": "Raw audio data (float32, mono, 16kHz expected)" }, + "timestamp": { "type": "double", "required": false, "description": "Timestamp when audio was captured (monotonic, seconds)" }, + "sample_rate": { "type": "int32", "required": false, "default": 16000, "description": "Sample rate in Hz" }, + "channels": { "type": "int32", "required": false, "default": 1, "description": "Number of channels (1 for mono)" } + } + }, + "TranscriptUpdate": { + "description": "Real-time transcript update from server", + "fields": { + "meeting_id": { "type": "string", "description": "Meeting ID this transcript belongs to" }, + "update_type": { "type": "UpdateType", "description": "Type of update (partial, final, VAD events)" }, + "partial_text": { "type": "string", "description": "For partial updates - tentative transcript text" }, + "segment": { "type": "FinalSegment", "description": "For final updates - confirmed transcript segment" }, + "server_timestamp": { "type": "double", "description": "Server-side processing timestamp" } + } + }, + "FinalSegment": { + "description": "Confirmed transcript segment with word-level timing", + "fields": { + "segment_id": { "type": "int32", "description": "Segment ID (sequential within meeting)" }, + "text": { "type": "string", "description": "Transcript text" }, + "start_time": { "type": "double", "description": "Start time relative to meeting start (seconds)" }, + "end_time": { "type": "double", "description": "End time relative to meeting start (seconds)" }, + "words": { "type": "array", "items": "WordTiming", "description": "Word-level timestamps" }, + "language": { "type": "string", "description": "Detected language code" }, + "language_confidence": { "type": "float", "description": "Language detection confidence (0.0-1.0)" }, + "avg_logprob": { "type": "float", "description": "Average log probability (quality indicator)" }, + "no_speech_prob": { "type": "float", "description": "Probability that segment contains no speech" }, + "speaker_id": { "type": "string", "description": "Speaker identification (from diarization)" }, + "speaker_confidence": { "type": "float", "description": "Speaker assignment confidence (0.0-1.0)" } + } + }, + "WordTiming": { + "description": "Word-level timing information", + "fields": { + "word": { "type": "string", "description": "The word text" }, + "start_time": { "type": "double", "description": "Start time in seconds" }, + "end_time": { "type": "double", "description": "End time in seconds" }, + "probability": { "type": "float", "description": "Recognition confidence (0.0-1.0)" } + } + }, + "Meeting": { + "description": "Complete meeting record with transcript and summary", + "fields": { + "id": { "type": "string", "description": "Unique meeting identifier (UUID)" }, + "title": { "type": "string", "description": "User-provided or auto-generated title" }, + "state": { "type": "MeetingState", "description": "Current meeting state" }, + "created_at": { "type": "double", "description": "Creation timestamp (Unix epoch seconds)" }, + "started_at": { "type": "double", "description": "Recording start timestamp" }, + "ended_at": { "type": "double", "description": "Recording end timestamp" }, + "duration_seconds": { "type": "double", "description": "Total duration in seconds" }, + "segments": { "type": "array", "items": "FinalSegment", "description": "Full transcript segments" }, + "summary": { "type": "Summary", "description": "Generated summary (if available)" }, + "metadata": { "type": "map", "description": "Custom metadata key-value pairs" } + } + }, + "CreateMeetingRequest": { + "description": "Request to create a new meeting", + "fields": { + "title": { "type": "string", "required": false, "description": "Optional title (auto-generated if not provided)" }, + "metadata": { "type": "map", "required": false, "description": "Optional custom metadata" } + } + }, + "StopMeetingRequest": { + "description": "Request to stop a meeting recording", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to stop" } + } + }, + "ListMeetingsRequest": { + "description": "Request to list meetings with filters", + "fields": { + "states": { "type": "array", "items": "MeetingState", "required": false, "description": "Filter by meeting states" }, + "limit": { "type": "int32", "required": false, "default": 50, "description": "Max results to return" }, + "offset": { "type": "int32", "required": false, "default": 0, "description": "Pagination offset" }, + "sort_order": { "type": "SortOrder", "required": false, "default": "SORT_ORDER_CREATED_DESC", "description": "Sort order" } + } + }, + "ListMeetingsResponse": { + "description": "Response containing list of meetings", + "fields": { + "meetings": { "type": "array", "items": "Meeting", "description": "List of meetings" }, + "total_count": { "type": "int32", "description": "Total number of meetings matching filter" } + } + }, + "GetMeetingRequest": { + "description": "Request to get a specific meeting", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to retrieve" }, + "include_segments": { "type": "bool", "required": false, "default": false, "description": "Include full transcript segments" }, + "include_summary": { "type": "bool", "required": false, "default": false, "description": "Include summary if available" } + } + }, + "DeleteMeetingRequest": { + "description": "Request to delete a meeting", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to delete" } + } + }, + "DeleteMeetingResponse": { + "description": "Response confirming meeting deletion", + "fields": { + "success": { "type": "bool", "description": "Whether deletion was successful" } + } + }, + "Summary": { + "description": "AI-generated meeting summary with evidence linking", + "fields": { + "meeting_id": { "type": "string", "description": "Meeting this summary belongs to" }, + "executive_summary": { "type": "string", "description": "Executive summary (2-3 sentences)" }, + "key_points": { "type": "array", "items": "KeyPoint", "description": "Key points/highlights extracted" }, + "action_items": { "type": "array", "items": "ActionItem", "description": "Action items extracted" }, + "generated_at": { "type": "double", "description": "Generation timestamp (Unix epoch)" }, + "model_version": { "type": "string", "description": "Model/version used for generation" } + } + }, + "KeyPoint": { + "description": "Key point from meeting with evidence linking", + "fields": { + "text": { "type": "string", "description": "The key point text" }, + "segment_ids": { "type": "array", "items": "int32", "description": "Segment IDs that support this point" }, + "start_time": { "type": "double", "description": "Start of relevant time range" }, + "end_time": { "type": "double", "description": "End of relevant time range" } + } + }, + "ActionItem": { + "description": "Action item extracted from meeting", + "fields": { + "text": { "type": "string", "description": "Action item description" }, + "assignee": { "type": "string", "description": "Person assigned (if mentioned)" }, + "due_date": { "type": "double", "description": "Due date (Unix epoch, if mentioned)" }, + "priority": { "type": "Priority", "description": "Priority level" }, + "segment_ids": { "type": "array", "items": "int32", "description": "Segment IDs mentioning this action" } + } + }, + "GenerateSummaryRequest": { + "description": "Request to generate meeting summary", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to summarize" }, + "force_regenerate": { "type": "bool", "required": false, "default": false, "description": "Force regeneration even if summary exists" } + } + }, + "Annotation": { + "description": "User-created annotation on meeting timeline", + "fields": { + "id": { "type": "string", "description": "Unique annotation identifier (UUID)" }, + "meeting_id": { "type": "string", "description": "Meeting this annotation belongs to" }, + "annotation_type": { "type": "AnnotationType", "description": "Type of annotation" }, + "text": { "type": "string", "description": "Annotation text content" }, + "start_time": { "type": "double", "description": "Start time relative to meeting start (seconds)" }, + "end_time": { "type": "double", "description": "End time relative to meeting start (seconds)" }, + "segment_ids": { "type": "array", "items": "int32", "description": "Linked transcript segment IDs" }, + "created_at": { "type": "double", "description": "Creation timestamp (Unix epoch)" } + } + }, + "AddAnnotationRequest": { + "description": "Request to add an annotation", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to annotate" }, + "annotation_type": { "type": "AnnotationType", "required": true, "description": "Type of annotation" }, + "text": { "type": "string", "required": true, "description": "Annotation text" }, + "start_time": { "type": "double", "required": true, "description": "Start time in seconds" }, + "end_time": { "type": "double", "required": true, "description": "End time in seconds" }, + "segment_ids": { "type": "array", "items": "int32", "required": false, "description": "Optional linked segment IDs" } + } + }, + "GetAnnotationRequest": { + "description": "Request to get an annotation", + "fields": { + "annotation_id": { "type": "string", "required": true, "description": "Annotation ID to retrieve" } + } + }, + "ListAnnotationsRequest": { + "description": "Request to list annotations for a meeting", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to list annotations for" }, + "start_time": { "type": "double", "required": false, "description": "Filter: start of time range" }, + "end_time": { "type": "double", "required": false, "description": "Filter: end of time range" } + } + }, + "ListAnnotationsResponse": { + "description": "Response containing annotations", + "fields": { + "annotations": { "type": "array", "items": "Annotation", "description": "List of annotations" } + } + }, + "UpdateAnnotationRequest": { + "description": "Request to update an annotation", + "fields": { + "annotation_id": { "type": "string", "required": true, "description": "Annotation ID to update" }, + "annotation_type": { "type": "AnnotationType", "required": false, "description": "New type (keeps existing if not set)" }, + "text": { "type": "string", "required": false, "description": "New text (keeps existing if empty)" }, + "start_time": { "type": "double", "required": false, "description": "New start time (keeps existing if 0)" }, + "end_time": { "type": "double", "required": false, "description": "New end time (keeps existing if 0)" }, + "segment_ids": { "type": "array", "items": "int32", "required": false, "description": "New segment IDs (replaces existing)" } + } + }, + "DeleteAnnotationRequest": { + "description": "Request to delete an annotation", + "fields": { + "annotation_id": { "type": "string", "required": true, "description": "Annotation ID to delete" } + } + }, + "DeleteAnnotationResponse": { + "description": "Response confirming annotation deletion", + "fields": { + "success": { "type": "bool", "description": "Whether deletion was successful" } + } + }, + "ExportTranscriptRequest": { + "description": "Request to export meeting transcript", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to export" }, + "format": { "type": "ExportFormat", "required": true, "description": "Export format (Markdown or HTML)" } + } + }, + "ExportTranscriptResponse": { + "description": "Response containing exported transcript", + "fields": { + "content": { "type": "string", "description": "Exported content as string" }, + "format_name": { "type": "string", "description": "Human-readable format name" }, + "file_extension": { "type": "string", "description": "Suggested file extension (.md or .html)" } + } + }, + "RefineSpeakerDiarizationRequest": { + "description": "Request to run offline speaker diarization", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID to process" }, + "num_speakers": { "type": "int32", "required": false, "description": "Known number of speakers (auto-detect if 0)" } + } + }, + "RefineSpeakerDiarizationResponse": { + "description": "Response from diarization job start", + "fields": { + "segments_updated": { "type": "int32", "description": "Number of segments updated (0 if job is async)" }, + "speaker_ids": { "type": "array", "items": "string", "description": "Distinct speaker IDs found" }, + "error_message": { "type": "string", "description": "Error message if failed" }, + "job_id": { "type": "string", "description": "Background job ID for polling" }, + "status": { "type": "JobStatus", "description": "Current job status" } + } + }, + "RenameSpeakerRequest": { + "description": "Request to rename a speaker", + "fields": { + "meeting_id": { "type": "string", "required": true, "description": "Meeting ID" }, + "old_speaker_id": { "type": "string", "required": true, "description": "Original speaker ID (e.g., 'SPEAKER_00')" }, + "new_speaker_name": { "type": "string", "required": true, "description": "New human-readable name (e.g., 'Alice')" } + } + }, + "RenameSpeakerResponse": { + "description": "Response from speaker rename", + "fields": { + "segments_updated": { "type": "int32", "description": "Number of segments updated" }, + "success": { "type": "bool", "description": "Whether rename was successful" } + } + }, + "GetDiarizationJobStatusRequest": { + "description": "Request to check diarization job status", + "fields": { + "job_id": { "type": "string", "required": true, "description": "Job ID from RefineSpeakerDiarization" } + } + }, + "DiarizationJobStatus": { + "description": "Status of a diarization job", + "fields": { + "job_id": { "type": "string", "description": "Job ID" }, + "status": { "type": "JobStatus", "description": "Current status" }, + "segments_updated": { "type": "int32", "description": "Segments updated (when completed)" }, + "speaker_ids": { "type": "array", "items": "string", "description": "Speaker IDs found (when completed)" }, + "error_message": { "type": "string", "description": "Error message if failed" } + } + }, + "ServerInfoRequest": { + "description": "Request for server info (empty message)", + "fields": {} + }, + "ServerInfo": { + "description": "Server health and capabilities", + "fields": { + "version": { "type": "string", "description": "Server version string" }, + "asr_model": { "type": "string", "description": "Loaded ASR model name" }, + "asr_ready": { "type": "bool", "description": "Whether ASR is ready" }, + "supported_sample_rates": { "type": "array", "items": "int32", "description": "Supported audio sample rates" }, + "max_chunk_size": { "type": "int32", "description": "Maximum audio chunk size in bytes" }, + "uptime_seconds": { "type": "double", "description": "Server uptime in seconds" }, + "active_meetings": { "type": "int32", "description": "Number of active meetings" }, + "diarization_enabled": { "type": "bool", "description": "Whether diarization is enabled" }, + "diarization_ready": { "type": "bool", "description": "Whether diarization models are ready" } + } + } + }, + "uiHints": { + "primaryEntities": ["Meeting", "Summary", "Annotation"], + "listViews": { + "meetings": { + "endpoint": "ListMeetings", + "displayFields": ["title", "state", "created_at", "duration_seconds"], + "actions": ["view", "delete", "export"] + }, + "annotations": { + "endpoint": "ListAnnotations", + "displayFields": ["annotation_type", "text", "start_time"], + "actions": ["edit", "delete"] + } + }, + "detailViews": { + "meeting": { + "endpoint": "GetMeeting", + "sections": [ + { "name": "Overview", "fields": ["title", "state", "created_at", "duration_seconds"] }, + { "name": "Transcript", "field": "segments", "type": "timeline" }, + { "name": "Summary", "field": "summary", "type": "expandable" } + ] + } + }, + "forms": { + "createMeeting": { + "endpoint": "CreateMeeting", + "fields": [ + { "name": "title", "label": "Meeting Title", "type": "text", "placeholder": "Optional - will be auto-generated" } + ] + }, + "addAnnotation": { + "endpoint": "AddAnnotation", + "fields": [ + { "name": "annotation_type", "label": "Type", "type": "select", "options": "AnnotationType" }, + { "name": "text", "label": "Note", "type": "textarea" }, + { "name": "start_time", "label": "Start Time", "type": "number" }, + { "name": "end_time", "label": "End Time", "type": "number" } + ] + } + }, + "realTimeFeatures": { + "transcription": { + "endpoint": "StreamTranscription", + "description": "Real-time audio-to-text with live updates", + "updateTypes": ["partial", "final", "vad_start", "vad_end"] + } + } + } +}