From 521629ec86e98ea2c8a7d5d513eafc974915e920 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Wed, 1 Apr 2026 09:21:26 +0100 Subject: [PATCH 01/59] Fix `any` type usage in `proprietary/` (#5949) # Description of Changes Follow on from #5934, expanding `any` type usage ban to the `proprietary/` folder --- engine/config/.env.example | 9 -- frontend/eslint.config.mjs | 5 +- .../proprietary/auth/springAuthClient.test.ts | 16 +- .../src/proprietary/auth/springAuthClient.ts | 4 +- .../shared/ChangeUserPasswordModal.tsx | 7 +- .../components/shared/DividerWithText.tsx | 2 +- .../components/shared/InviteMembersModal.tsx | 22 +-- .../components/shared/ManageBillingButton.tsx | 4 +- .../components/shared/UpgradeBanner.tsx | 6 +- .../configSections/AdminAdvancedSection.tsx | 18 +-- .../configSections/AdminAuditSection.tsx | 5 +- .../AdminConnectionsSection.tsx | 137 ++++++++++++------ .../configSections/AdminDatabaseSection.tsx | 31 ++-- .../configSections/AdminFeaturesSection.tsx | 6 +- .../configSections/AdminGeneralSection.tsx | 8 +- .../configSections/AdminPlanSection.tsx | 4 +- .../configSections/AdminPrivacySection.tsx | 6 +- .../configSections/AdminSecuritySection.tsx | 8 +- .../configSections/AdminUsageSection.tsx | 4 +- .../config/configSections/PeopleSection.tsx | 55 ++++--- .../configSections/TeamDetailsSection.tsx | 48 +++--- .../config/configSections/TeamsSection.tsx | 29 ++-- .../configSections/audit/AuditEventsTable.tsx | 18 +-- .../audit/AuditExportSection.tsx | 2 +- .../configSections/audit/AuditFiltersForm.tsx | 10 +- .../stripeCheckout/hooks/useLicensePolling.ts | 4 +- .../components/workflow/ParticipantView.tsx | 8 +- .../contexts/ServerExperienceContext.tsx | 14 +- .../hooks/workflow/useParticipantSession.ts | 29 ++-- .../src/proprietary/routes/InviteAccept.tsx | 25 ++-- .../proprietary/routes/ShareLinkLoader.tsx | 7 +- .../src/proprietary/routes/ShareLinkPage.tsx | 13 +- .../proprietary/services/shareLinkImport.ts | 8 +- .../src/proprietary/services/teamService.ts | 22 ++- .../services/userManagementService.ts | 22 +-- 35 files changed, 341 insertions(+), 275 deletions(-) delete mode 100644 engine/config/.env.example diff --git a/engine/config/.env.example b/engine/config/.env.example deleted file mode 100644 index 64fb0e67ae..0000000000 --- a/engine/config/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Configure the model strings passed to pydantic-ai. Provider credentials are handled by -# pydantic-ai and should be set using the provider's native environment variables, for example -# ANTHROPIC_API_KEY or OPENAI_API_KEY. -STIRLING_SMART_MODEL=anthropic:claude-haiku-4-5 -STIRLING_FAST_MODEL=anthropic:claude-haiku-4-5 - -# Default output token limits applied by the engine for each model tier. -STIRLING_SMART_MODEL_MAX_TOKENS=8192 -STIRLING_FAST_MODEL_MAX_TOKENS=2048 diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 6f0ed90cbb..efe45f17b5 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -83,7 +83,10 @@ export default defineConfig( }, // Folders that have been cleaned up and are now conformant - stricter rules enforced here { - files: ['src/saas/**/*.{js,mjs,jsx,ts,tsx}'], + files: [ + 'src/proprietary/**/*.{js,mjs,jsx,ts,tsx}', + 'src/saas/**/*.{js,mjs,jsx,ts,tsx}', + ], languageOptions: { parserOptions: { project: true, diff --git a/frontend/src/proprietary/auth/springAuthClient.test.ts b/frontend/src/proprietary/auth/springAuthClient.test.ts index cae070373a..40d7ef9da5 100644 --- a/frontend/src/proprietary/auth/springAuthClient.test.ts +++ b/frontend/src/proprietary/auth/springAuthClient.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { springAuth } from '@app/auth/springAuthClient'; import { startOAuthNavigation } from '@app/extensions/oauthNavigation'; import apiClient from '@app/services/apiClient'; -import { AxiosError } from 'axios'; +import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'; // Mock apiClient vi.mock('@app/services/apiClient'); @@ -45,7 +45,7 @@ describe('SpringAuthClient', () => { vi.mocked(apiClient.get).mockResolvedValueOnce({ status: 200, data: { user: mockUser }, - } as any); + } as unknown as AxiosResponse); const result = await springAuth.getSession(); @@ -74,7 +74,7 @@ describe('SpringAuthClient', () => { statusText: 'Unauthorized', data: {}, headers: {}, - config: {} as any, + config: {} as InternalAxiosRequestConfig, } ); @@ -102,7 +102,7 @@ describe('SpringAuthClient', () => { statusText: 'Forbidden', data: {}, headers: {}, - config: {} as any, + config: {} as InternalAxiosRequestConfig, } ); @@ -141,7 +141,7 @@ describe('SpringAuthClient', () => { expires_in: 3600, }, }, - } as any); + } as unknown as AxiosResponse); // Spy on window.dispatchEvent const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent'); @@ -208,7 +208,7 @@ describe('SpringAuthClient', () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ status: 200, data: { user: mockUser }, - } as any); + } as unknown as AxiosResponse); const result = await springAuth.signUp(credentials); @@ -259,7 +259,7 @@ describe('SpringAuthClient', () => { vi.mocked(apiClient.post).mockResolvedValueOnce({ status: 200, data: {}, - } as any); + } as unknown as AxiosResponse); const result = await springAuth.signOut(); @@ -308,7 +308,7 @@ describe('SpringAuthClient', () => { expires_in: 3600, }, }, - } as any); + } as unknown as AxiosResponse); const result = await springAuth.refreshSession(); diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index 373df79c76..b97191beee 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -85,7 +85,7 @@ export interface User { is_anonymous?: boolean; isFirstLogin?: boolean; authenticationType?: string; - app_metadata?: Record; + app_metadata?: Record; } export interface Session { @@ -447,7 +447,7 @@ class SpringAuthClient { */ async signInWithOAuth(params: { provider: OAuthProvider; - options?: { redirectTo?: string; queryParams?: Record }; + options?: { redirectTo?: string; queryParams?: Record }; }): Promise<{ error: AuthError | null }> { try { const redirectPath = normalizeRedirectPath(params.options?.redirectTo); diff --git a/frontend/src/proprietary/components/shared/ChangeUserPasswordModal.tsx b/frontend/src/proprietary/components/shared/ChangeUserPasswordModal.tsx index 9e0cd222c4..27f7741a0a 100644 --- a/frontend/src/proprietary/components/shared/ChangeUserPasswordModal.tsx +++ b/frontend/src/proprietary/components/shared/ChangeUserPasswordModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { isAxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; import { ActionIcon, @@ -125,8 +126,10 @@ export default function ChangeUserPasswordModal({ opened, onClose, user, onSucce alert({ alertType: 'success', title: t('workspace.people.changePassword.success', 'Password updated successfully') }); onSuccess(); handleClose(); - } catch (error: any) { - const errorMessage = error.response?.data?.message || error.response?.data?.error || error.message || t('workspace.people.changePassword.error', 'Failed to update password'); + } catch (error: unknown) { + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.changePassword.error', 'Failed to update password'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); diff --git a/frontend/src/proprietary/components/shared/DividerWithText.tsx b/frontend/src/proprietary/components/shared/DividerWithText.tsx index 888f4acfb7..df1e98583f 100644 --- a/frontend/src/proprietary/components/shared/DividerWithText.tsx +++ b/frontend/src/proprietary/components/shared/DividerWithText.tsx @@ -12,7 +12,7 @@ interface TextDividerProps { export default function DividerWithText({ text, className = '', style, variant = 'default', respondsToDarkMode = true, opacity }: TextDividerProps) { const variantClass = variant === 'subcategory' ? 'subcategory' : ''; const themeClass = respondsToDarkMode ? '' : 'force-light'; - const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as any]: opacity } : style; + const styleWithOpacity = opacity !== undefined ? { ...(style || {}), ['--text-divider-opacity' as string]: opacity } : style; if (text) { return ( diff --git a/frontend/src/proprietary/components/shared/InviteMembersModal.tsx b/frontend/src/proprietary/components/shared/InviteMembersModal.tsx index 3298ca8e94..24c522acc1 100644 --- a/frontend/src/proprietary/components/shared/InviteMembersModal.tsx +++ b/frontend/src/proprietary/components/shared/InviteMembersModal.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react'; +import { isAxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; import { Modal, @@ -158,9 +159,11 @@ export default function InviteMembersModal({ opened, onClose, onSuccess }: Invit forceChange: false, forceMFA: false, }); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to invite user:', error); - const errorMessage = error.response?.data?.message || error.response?.data?.error || error.message || t('workspace.people.addMember.error'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.addMember.error'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -211,12 +214,11 @@ export default function InviteMembersModal({ opened, onClose, onSuccess }: Invit body: response.errors || response.error }); } - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to invite users:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.emailInvite.error', 'Failed to send invites'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.emailInvite.error', 'Failed to send invites'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -239,9 +241,11 @@ export default function InviteMembersModal({ opened, onClose, onSuccess }: Invit if (inviteLinkForm.sendEmail && inviteLinkForm.email) { alert({ alertType: 'success', title: t('workspace.people.inviteLink.emailSent', 'Invite link generated and sent via email') }); } - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to generate invite link:', error); - const errorMessage = error.response?.data?.message || error.response?.data?.error || error.message || t('workspace.people.inviteLink.error', 'Failed to generate invite link'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.inviteLink.error', 'Failed to generate invite link'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); diff --git a/frontend/src/proprietary/components/shared/ManageBillingButton.tsx b/frontend/src/proprietary/components/shared/ManageBillingButton.tsx index fc523f62f2..bdbf49beca 100644 --- a/frontend/src/proprietary/components/shared/ManageBillingButton.tsx +++ b/frontend/src/proprietary/components/shared/ManageBillingButton.tsx @@ -34,12 +34,12 @@ export const ManageBillingButton: React.FC = ({ // Open billing portal in new tab window.open(response.url, '_blank'); setLoading(false); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to open billing portal:', error); alert({ alertType: 'error', title: t('billing.portal.error', 'Failed to open billing portal'), - body: error.message || 'Please try again or contact support.', + body: (error instanceof Error ? error.message : undefined) || 'Please try again or contact support.', }); setLoading(false); } diff --git a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx index 5b31eedcaa..e5d65bd4b5 100644 --- a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx +++ b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx @@ -86,7 +86,7 @@ const UpgradeBanner: React.FC = () => { const scenarioProvidesInfo = scenarioKey && scenarioKey !== 'unknown' && scenarioKey !== 'licensed'; const derivedIsAdmin = scenarioProvidesInfo - ? scenarioKey!.includes('admin') + ? scenarioKey.includes('admin') : isAdmin; const derivedHasPaidLicense = scenarioKey === 'licensed' @@ -95,10 +95,10 @@ const UpgradeBanner: React.FC = () => { ? hasPaidLicense : false; const derivedIsUnderLimit = scenarioProvidesInfo - ? scenarioKey!.includes('under-limit') + ? scenarioKey.includes('under-limit') : isUnderLimit === true; const derivedIsOverLimit = scenarioProvidesInfo - ? scenarioKey!.includes('over-limit') + ? scenarioKey.includes('over-limit') : isOverLimit === true; const effectiveIsAdmin = scenario diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx index 033abd326a..ff297b4a48 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { isAxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Accordion, TextInput, MultiSelect } from '@mantine/core'; import { alert } from '@app/components/toast'; @@ -72,7 +73,7 @@ export default function AdminAdvancedSection() { isFieldPending, } = useAdminSettings({ sectionName: 'advanced', - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { const [systemResponse, processExecutorResponse] = await Promise.all([ apiClient.get('/api/v1/admin/settings/section/system'), apiClient.get('/api/v1/admin/settings/section/processExecutor') @@ -81,7 +82,7 @@ export default function AdminAdvancedSection() { const systemData = systemResponse.data || {}; const processExecutorData = processExecutorResponse.data || {}; - const result: AdvancedSettingsData & { _pending?: Record } = { + const result: AdvancedSettingsData & { _pending?: Record } = { enableAlphaFunctionality: systemData.enableAlphaFunctionality || false, maxDPI: systemData.maxDPI || 0, enableUrlToPDF: systemData.enableUrlToPDF || false, @@ -101,7 +102,7 @@ export default function AdminAdvancedSection() { }; // Merge pending blocks from both endpoints - const pendingBlock: Record = {}; + const pendingBlock: Record = {}; if (systemData._pending?.enableAlphaFunctionality !== undefined) { pendingBlock.enableAlphaFunctionality = systemData._pending.enableAlphaFunctionality; } @@ -131,7 +132,7 @@ export default function AdminAdvancedSection() { return result; }, saveTransformer: (settings) => { - const deltaSettings: Record = { + const deltaSettings: Record = { 'system.enableAlphaFunctionality': settings.enableAlphaFunctionality, 'system.maxDPI': settings.maxDPI, 'system.enableUrlToPDF': settings.enableUrlToPDF, @@ -281,9 +282,8 @@ export default function AdminAdvancedSection() { setManualDownloadLinks([]); } catch (error) { console.error('[AdminAdvancedSection] Download tessdata languages failed', error); - const response = (error as any)?.response; - const status = response?.status; - const serverMessage = response?.data?.message; + const status = isAxiosError(error) ? error.response?.status : undefined; + const serverMessage = isAxiosError(error) ? error.response?.data?.message : undefined; if (status === 403) { console.warn('[AdminAdvancedSection] Tessdata directory not writable, falling back to manual download:', serverMessage); @@ -309,12 +309,12 @@ export default function AdminAdvancedSection() { } let message: string; - if (!response) { + if (!isAxiosError(error) || !error.response) { message = t( 'admin.settings.advanced.tessdataDir.downloadErrorNetwork', 'Download failed due to a network error. Please check your connection and try again.' ); - } else if (status >= 500) { + } else if (status !== undefined && status >= 500) { message = t( 'admin.settings.advanced.tessdataDir.downloadErrorServer', 'The server encountered an error while downloading tessdata languages. Please try again later.' diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx index 53939a45cb..06703d813c 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import { Tabs, Loader, Alert, Stack, Text, Button, Accordion } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -35,9 +36,9 @@ const AdminAuditSection: React.FC = () => { setError(null); const status = await auditService.getSystemStatus(); setSystemStatus(status); - } catch (err: any) { + } catch (err: unknown) { // Check if this is a permission/license error (403/404) - const status = err?.response?.status; + const status = isAxiosError(err) ? err.response?.status : undefined; if (status === 403 || status === 404) { setError('enterprise-license-required'); } else { diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx index f3cec6a1c7..fd60e75cba 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -44,6 +44,66 @@ interface TelegramSettingsData { feedback?: FeedbackSettings; } +interface MailSettings { + enabled?: boolean; + enableInvites?: boolean; + host?: string; + port?: number; + username?: string; + password?: string; + from?: string; +} + +interface GoogleDriveSettings { + enabled?: boolean; + clientId?: string; + apiKey?: string; + appId?: string; +} + +interface OAuth2GenericSettings { + enabled?: boolean; + provider?: string; + issuer?: string; + clientId?: string; + clientSecret?: string; + scopes?: string; + useAsUsername?: string; + autoCreateUser?: boolean; + blockRegistration?: boolean; +} + +interface Saml2Settings { + enabled?: boolean; + provider?: string; + registrationId?: string; + idpMetadataUri?: string; + idpSingleLoginUrl?: string; + idpSingleLogoutUrl?: string; + idpIssuer?: string; + idpCert?: string; + privateKey?: string; + spCert?: string; + autoCreateUser?: boolean; + blockRegistration?: boolean; +} + +interface OAuth2ClientSettings { + clientId?: string; + clientSecret?: string; + scopes?: string; + useAsUsername?: string; + issuer?: string; +} + +type ProviderSettings = + | MailSettings + | TelegramSettingsData + | GoogleDriveSettings + | OAuth2GenericSettings + | Saml2Settings + | OAuth2ClientSettings; + interface ConnectionsSettingsData { oauth2?: { enabled?: boolean; @@ -55,22 +115,10 @@ interface ConnectionsSettingsData { blockRegistration?: boolean; useAsUsername?: string; scopes?: string; - client?: { - [key: string]: any; - }; - }; - saml2?: { - [key: string]: any; - }; - mail?: { - enabled?: boolean; - enableInvites?: boolean; - host?: string; - port?: number; - username?: string; - password?: string; - from?: string; + client?: Record; }; + saml2?: Saml2Settings; + mail?: MailSettings; telegram?: TelegramSettingsData; ssoAutoLogin?: boolean; enableMobileScanner?: boolean; @@ -93,7 +141,7 @@ export default function AdminConnectionsSection() { const adminSettings = useAdminSettings({ sectionName: 'connections', - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { // Fetch security settings (oauth2, saml2) const securityResponse = await apiClient.get('/api/v1/admin/settings/section/security'); const securityData = securityResponse.data || {}; @@ -114,7 +162,7 @@ export default function AdminConnectionsSection() { const systemResponse = await apiClient.get('/api/v1/admin/settings/section/system'); const systemData = systemResponse.data || {}; - const result: ConnectionsSettingsData & { _pending?: Record } = { + const result: ConnectionsSettingsData & { _pending?: Record } = { oauth2: securityData.oauth2 || {}, saml2: securityData.saml2 || {}, mail: mailData || {}, @@ -132,7 +180,7 @@ export default function AdminConnectionsSection() { }; // Merge pending blocks from all endpoints - const pendingBlock: Record = {}; + const pendingBlock: Record = {}; if (securityData._pending?.oauth2) { pendingBlock.oauth2 = securityData._pending.oauth2; } @@ -183,13 +231,13 @@ export default function AdminConnectionsSection() { return result; }, saveTransformer: (currentSettings: ConnectionsSettingsData) => { - const deltaSettings: Record = {}; + const deltaSettings: Record = {}; // Build delta for oauth2 settings if (currentSettings.oauth2) { Object.keys(currentSettings.oauth2).forEach((key) => { if (key !== 'client') { - deltaSettings[`security.oauth2.${key}`] = (currentSettings.oauth2 as Record)[key]; + deltaSettings[`security.oauth2.${key}`] = (currentSettings.oauth2 as Record)[key]; } }); @@ -197,7 +245,7 @@ export default function AdminConnectionsSection() { const oauth2Client = currentSettings.oauth2.client; if (oauth2Client) { Object.keys(oauth2Client).forEach((providerId) => { - const providerSettings = oauth2Client[providerId]; + const providerSettings = oauth2Client[providerId] as Record; Object.keys(providerSettings).forEach((key) => { deltaSettings[`security.oauth2.client.${providerId}.${key}`] = providerSettings[key]; }); @@ -207,22 +255,25 @@ export default function AdminConnectionsSection() { // Build delta for saml2 settings if (currentSettings.saml2) { - Object.keys(currentSettings.saml2).forEach((key) => { - deltaSettings[`security.saml2.${key}`] = (currentSettings.saml2 as Record)[key]; + const saml2 = currentSettings.saml2 as Record; + Object.keys(saml2).forEach((key) => { + deltaSettings[`security.saml2.${key}`] = saml2[key]; }); } // Mail settings if (currentSettings.mail) { - Object.keys(currentSettings.mail).forEach((key) => { - deltaSettings[`mail.${key}`] = (currentSettings.mail as Record)[key]; + const mail = currentSettings.mail as Record; + Object.keys(mail).forEach((key) => { + deltaSettings[`mail.${key}`] = mail[key]; }); } // Telegram settings if (currentSettings.telegram) { - Object.keys(currentSettings.telegram).forEach((key) => { - deltaSettings[`telegram.${key}`] = (currentSettings.telegram as Record)[key]; + const telegram = currentSettings.telegram as Record; + Object.keys(telegram).forEach((key) => { + deltaSettings[`telegram.${key}`] = telegram[key]; }); } @@ -332,7 +383,7 @@ export default function AdminConnectionsSection() { return !!(providerSettings?.clientId); }; - const getProviderSettings = (provider: Provider): Record => { + const getProviderSettings = (provider: Provider): ProviderSettings => { if (provider.id === 'saml2') { return settings?.saml2 || {}; } @@ -346,17 +397,17 @@ export default function AdminConnectionsSection() { } if (provider.id === 'googledrive') { - return { + const gd: GoogleDriveSettings = { enabled: settings?.googleDriveEnabled, clientId: settings?.googleDriveClientId, apiKey: settings?.googleDriveApiKey, appId: settings?.googleDriveAppId, }; + return gd; } if (provider.id === 'oauth2-generic') { - // Generic OAuth2 settings are at the root oauth2 level - return { + const generic: OAuth2GenericSettings = { enabled: settings?.oauth2?.enabled, provider: settings?.oauth2?.provider, issuer: settings?.oauth2?.issuer, @@ -367,6 +418,7 @@ export default function AdminConnectionsSection() { autoCreateUser: settings?.oauth2?.autoCreateUser, blockRegistration: settings?.oauth2?.blockRegistration, }; + return generic; } // Specific OAuth2 provider settings @@ -386,32 +438,35 @@ export default function AdminConnectionsSection() { const linkedProviders = allProviders.filter((p) => isProviderConfigured(p)); const availableProviders = allProviders.filter((p) => !isProviderConfigured(p)); - const updateProviderSettings = (provider: Provider, updatedSettings: Record) => { + const updateProviderSettings = (provider: Provider, updatedSettings: Record) => { if (provider.id === 'smtp') { - setSettings({ ...settings, mail: updatedSettings }); + setSettings({ ...settings, mail: updatedSettings as MailSettings }); } else if (provider.id === 'telegram') { - setSettings({ ...settings, telegram: updatedSettings }); + setSettings({ ...settings, telegram: updatedSettings as TelegramSettingsData }); } else if (provider.id === 'googledrive') { + const gd = updatedSettings as GoogleDriveSettings; setSettings({ ...settings, - googleDriveEnabled: updatedSettings.enabled, - googleDriveClientId: updatedSettings.clientId, - googleDriveApiKey: updatedSettings.apiKey, - googleDriveAppId: updatedSettings.appId, + googleDriveEnabled: gd.enabled, + googleDriveClientId: gd.clientId, + googleDriveApiKey: gd.apiKey, + googleDriveAppId: gd.appId, }); } else if (provider.id === 'saml2') { - setSettings({ ...settings, saml2: updatedSettings }); + setSettings({ ...settings, saml2: updatedSettings as Saml2Settings }); } else if (provider.id === 'oauth2-generic') { - setSettings({ ...settings, oauth2: updatedSettings }); + const generic = updatedSettings as OAuth2GenericSettings; + setSettings({ ...settings, oauth2: { ...settings.oauth2, ...generic } }); } else { // Specific OAuth2 provider + const clientSettings = updatedSettings as OAuth2ClientSettings; setSettings({ ...settings, oauth2: { ...settings.oauth2, client: { ...settings.oauth2?.client, - [provider.id]: updatedSettings + [provider.id]: clientSettings, } } }); diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx index a654f5abd3..749271d01c 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react"; +import { isAxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { NumberInput, @@ -68,7 +69,7 @@ export default function AdminDatabaseSection() { const { settings, setSettings, loading, saving, fetchSettings, saveSettings, isFieldPending } = useAdminSettings({ sectionName: "database", - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { const response = await apiClient.get("/api/v1/admin/settings/section/system"); const systemData = response.data || {}; @@ -85,7 +86,7 @@ export default function AdminDatabaseSection() { }; // Map pending changes from system._pending.datasource to root level - const result: DatabaseSettingsData & { _pending?: Record } = { ...datasource }; + const result: DatabaseSettingsData & { _pending?: Record } = { ...datasource }; if (systemData._pending?.datasource) { result._pending = systemData._pending.datasource; } @@ -94,7 +95,7 @@ export default function AdminDatabaseSection() { }, saveTransformer: (settings: DatabaseSettingsData) => { // Convert flat settings to dot-notation for delta endpoint - const deltaSettings: Record = { + const deltaSettings: Record = { "system.datasource.enableCustomDatabase": settings.enableCustomDatabase, "system.datasource.customDatabaseUrl": settings.customDatabaseUrl, "system.datasource.username": settings.username, @@ -141,8 +142,8 @@ export default function AdminDatabaseSection() { const data = await databaseManagementService.getDatabaseData(); setBackupFiles(data.backupFiles || []); setDatabaseVersion(data.databaseVersion || null); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; + } catch (error: unknown) { + const message = isAxiosError(error) ? (error.response?.data?.message || error.message) : undefined; alert({ alertType: "error", title: t("admin.settings.database.loadError", "Failed to load database backups"), @@ -189,8 +190,8 @@ export default function AdminDatabaseSection() { await databaseManagementService.createBackup(); alert({ alertType: "success", title: t("admin.settings.database.backupCreated", "Backup created successfully") }); await loadBackupData(); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; + } catch (error: unknown) { + const message = isAxiosError(error) ? (error.response?.data?.message || error.message) : undefined; alert({ alertType: "error", title: t("admin.settings.database.backupFailed", "Failed to create backup"), @@ -209,8 +210,8 @@ export default function AdminDatabaseSection() { alert({ alertType: "success", title: t("admin.settings.database.importSuccess", "Backup imported successfully") }); setUploadFile(null); await loadBackupData(); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; + } catch (error: unknown) { + const message = isAxiosError(error) ? (error.response?.data?.message || error.message) : undefined; alert({ alertType: "error", title: t("admin.settings.database.importFailed", "Failed to import backup"), @@ -270,8 +271,8 @@ export default function AdminDatabaseSection() { await databaseManagementService.importFromFileName(fileName); alert({ alertType: "success", title: t("admin.settings.database.importSuccess", "Backup imported successfully") }); await loadBackupData(); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; + } catch (error: unknown) { + const message = isAxiosError(error) ? (error.response?.data?.message || error.message) : undefined; alert({ alertType: "error", title: t("admin.settings.database.importFailed", "Failed to import backup"), @@ -289,8 +290,8 @@ export default function AdminDatabaseSection() { await databaseManagementService.deleteBackup(fileName); alert({ alertType: "success", title: t("admin.settings.database.deleteSuccess", "Backup deleted") }); await loadBackupData(); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; + } catch (error: unknown) { + const message = isAxiosError(error) ? (error.response?.data?.message || error.message) : undefined; alert({ alertType: "error", title: t("admin.settings.database.deleteFailed", "Failed to delete backup"), @@ -320,8 +321,8 @@ export default function AdminDatabaseSection() { link.download = fileName; document.body.appendChild(link); link.click(); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; + } catch (error: unknown) { + const message = isAxiosError(error) ? (error.response?.data?.message || error.message) : undefined; alert({ alertType: "error", title: t("admin.settings.database.downloadFailed", "Failed to download backup"), diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx index ed9b6a1300..7da712f76a 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx @@ -38,11 +38,11 @@ export default function AdminFeaturesSection() { isFieldPending, } = useAdminSettings({ sectionName: 'features', - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { const systemResponse = await apiClient.get('/api/v1/admin/settings/section/system'); const systemData = systemResponse.data || {}; - const result: FeaturesSettingsData & { _pending?: Record } = { + const result: FeaturesSettingsData & { _pending?: Record } = { serverCertificate: systemData.serverCertificate || { enabled: true, organizationName: 'Stirling-PDF', @@ -59,7 +59,7 @@ export default function AdminFeaturesSection() { return result; }, saveTransformer: (settings: FeaturesSettingsData) => { - const deltaSettings: Record = {}; + const deltaSettings: Record = {}; if (settings.serverCertificate) { deltaSettings['system.serverCertificate.enabled'] = settings.serverCertificate.enabled; diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx index 9ca8a60303..39d1758dfd 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx @@ -91,7 +91,7 @@ export default function AdminGeneralSection() { isFieldPending, } = useAdminSettings({ sectionName: 'general', - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { const [uiResponse, systemResponse, premiumResponse] = await Promise.all([ apiClient.get('/api/v1/admin/settings/section/ui'), apiClient.get('/api/v1/admin/settings/section/system'), @@ -113,7 +113,7 @@ export default function AdminGeneralSection() { ? watchedFoldersDirs : (pipelinePaths.watchedFoldersDir ? [pipelinePaths.watchedFoldersDir] : []); - const result: GeneralSettingsData & { _pending?: Record } = { + const result: GeneralSettingsData & { _pending?: Record } = { ui, system, customPaths: { @@ -140,7 +140,7 @@ export default function AdminGeneralSection() { }; // Merge pending blocks from all three endpoints - const pendingBlock: Record = {}; + const pendingBlock: Record = {}; if (ui._pending) { pendingBlock.ui = ui._pending; } @@ -161,7 +161,7 @@ export default function AdminGeneralSection() { return result; }, saveTransformer: (settings: GeneralSettingsData) => { - const deltaSettings: Record = { + const deltaSettings: Record = { // UI settings 'ui.appNameNavbar': settings.ui?.appNameNavbar, 'ui.languages': settings.ui?.languages, diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx index 0b47cf1481..d1f42870fa 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx @@ -72,12 +72,12 @@ const AdminPlanSection: React.FC = () => { // Open billing portal in new tab window.open(response.url, '_blank'); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to open billing portal:', error); alert({ alertType: 'error', title: t('billing.portal.error', 'Failed to open billing portal'), - body: error.message || 'Please try again or contact support.', + body: (error instanceof Error ? error.message : undefined) || 'Please try again or contact support.', }); } }, [licenseInfo, t, validateLoginEnabled]); diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx index aea2593bfe..80c592f96d 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx @@ -33,7 +33,7 @@ export default function AdminPrivacySection() { isFieldPending, } = useAdminSettings({ sectionName: 'privacy', - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { const [metricsResponse, systemResponse] = await Promise.all([ apiClient.get('/api/v1/admin/settings/section/metrics'), apiClient.get('/api/v1/admin/settings/section/system') @@ -42,14 +42,14 @@ export default function AdminPrivacySection() { const metrics = metricsResponse.data; const system = systemResponse.data; - const result: PrivacySettingsData & { _pending?: Record } = { + const result: PrivacySettingsData & { _pending?: Record } = { enableAnalytics: system.enableAnalytics || false, googleVisibility: system.googlevisibility || false, metricsEnabled: metrics.enabled || false }; // Merge pending blocks from both endpoints - const pendingBlock: Record = {}; + const pendingBlock: Record = {}; if (system._pending?.enableAnalytics !== undefined) { pendingBlock.enableAnalytics = system._pending.enableAnalytics; } diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx index 29dc0c211f..f915d9d695 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx @@ -67,7 +67,7 @@ export default function AdminSecuritySection() { isFieldPending, } = useAdminSettings({ sectionName: 'security', - fetchTransformer: async (): Promise }> => { + fetchTransformer: async (): Promise }> => { const [securityResponse, premiumResponse, systemResponse] = await Promise.all([ apiClient.get('/api/v1/admin/settings/section/security'), apiClient.get('/api/v1/admin/settings/section/premium'), @@ -93,7 +93,7 @@ export default function AdminSecuritySection() { systemPending: JSON.parse(JSON.stringify(systemPending || {})) }); - const combined: SecuritySettingsData & { _pending?: Record } = { + const combined: SecuritySettingsData & { _pending?: Record } = { ...securityActive }; @@ -108,7 +108,7 @@ export default function AdminSecuritySection() { } // Merge all _pending blocks - const mergedPending: Record = {}; + const mergedPending: Record = {}; if (securityPending) { Object.assign(mergedPending, securityPending); } @@ -128,7 +128,7 @@ export default function AdminSecuritySection() { saveTransformer: (settings: SecuritySettingsData) => { const { audit, html, ...securitySettings } = settings; - const deltaSettings: Record = { + const deltaSettings: Record = { // Security settings 'security.enableLogin': securitySettings.enableLogin, 'security.loginMethod': securitySettings.loginMethod, diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx index fe551c8bc8..a0db28e137 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminUsageSection.tsx @@ -155,10 +155,10 @@ const AdminUsageSection: React.FC = () => { const displayedVisits = endpoints.reduce((sum, e) => sum + e.visits, 0); const totalVisits = Number.isFinite(data?.totalVisits) - ? Math.max(0, data?.totalVisits as number) + ? Math.max(0, data?.totalVisits ?? 0) : displayedVisits; const totalEndpoints = Number.isFinite(data?.totalEndpoints) - ? Math.max(0, data?.totalEndpoints as number) + ? Math.max(0, data?.totalEndpoints ?? 0) : endpoints.length; const displayedPercentage = totalVisits > 0 diff --git a/frontend/src/proprietary/components/shared/config/configSections/PeopleSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/PeopleSection.tsx index 2b784511c7..4b45d4fa6f 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/PeopleSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/PeopleSection.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; import { Stack, @@ -17,6 +18,7 @@ import { CloseButton, Avatar, Box, + type ComboboxItem, } from '@mantine/core'; import LocalIcon from '@app/components/shared/LocalIcon'; import { alert } from '@app/components/toast'; @@ -114,7 +116,7 @@ export default function PeopleSection() { ...user, isActive: adminData.userSessions[user.username] || false, lastRequest: adminData.userLastRequest[user.username] || undefined, - mfaEnabled: adminData.userSettings?.[user.username]?.mfaEnabled === 'true', + mfaEnabled: (adminData.userSettings?.[user.username] as Record | undefined)?.mfaEnabled === 'true', })); setUsers(enrichedUsers); @@ -225,12 +227,11 @@ export default function PeopleSection() { alert({ alertType: 'success', title: t('workspace.people.editMember.success') }); closeEditModal(); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('[PeopleSection] Failed to update user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.editMember.error'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.editMember.error'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -242,12 +243,11 @@ export default function PeopleSection() { await userManagementService.toggleUserEnabled(user.username, !user.enabled); alert({ alertType: 'success', title: t('workspace.people.toggleEnabled.success') }); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('[PeopleSection] Failed to toggle user status:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.toggleEnabled.error'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.toggleEnabled.error'); alert({ alertType: 'error', title: errorMessage }); } }; @@ -262,12 +262,12 @@ export default function PeopleSection() { await userManagementService.deleteUser(user.username); alert({ alertType: 'success', title: t('workspace.people.deleteUserSuccess', 'User deleted successfully') }); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('[PeopleSection] Failed to delete user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.deleteUserError', 'Failed to delete user'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || + t('workspace.people.deleteUserError', 'Failed to delete user'); alert({ alertType: 'error', title: errorMessage }); } }; @@ -282,12 +282,11 @@ export default function PeopleSection() { await userManagementService.unlockUser(user.username); alert({ alertType: 'success', title: t('workspace.people.unlockUserSuccess', 'User account unlocked successfully') }); fetchData(); - } catch (error: any) { + } catch (error: unknown) { console.error('[PeopleSection] Failed to unlock user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.unlockUserError', 'Failed to unlock user account'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.unlockUserError', 'Failed to unlock user account'); alert({ alertType: 'error', title: errorMessage }); } }; @@ -339,9 +338,9 @@ export default function PeopleSection() { }, ]; - const renderRoleOption = ({ option }: { option: any }) => ( + const renderRoleOption = ({ option }: { option: ComboboxItem & { icon?: string; description?: string } }) => ( - + {option.label} @@ -668,12 +667,12 @@ export default function PeopleSection() { try { await userManagementService.disableMfaByAdmin(user.username); alert({ alertType: 'success', title: t('workspace.people.mfa.adminDisableSuccess', 'MFA disabled successfully for user') }); - } catch (error: any) { + } catch (error: unknown) { console.error('[PeopleSection] Failed to disable MFA for user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.mfa.adminDisableError', 'Failed to disable MFA for user'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || + t('workspace.people.mfa.adminDisableError', 'Failed to disable MFA for user'); alert({ alertType: 'error', title: errorMessage }); } }} diff --git a/frontend/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx index dd1f493a43..faf80d6a57 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; import { Stack, @@ -110,12 +111,11 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio setAddMemberModalOpened(false); setSelectedUserId(''); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to add member:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.addMemberToTeam.error', 'Failed to add user to team'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.teams.addMemberToTeam.error', 'Failed to add user to team'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -140,12 +140,11 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio await teamService.moveUserToTeam(user.username, user.rolesAsString || 'ROLE_USER', defaultTeam.id); alert({ alertType: 'success', title: t('workspace.teams.removeMemberSuccess', 'User removed from team') }); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to remove member:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.removeMemberError', 'Failed to remove user from team'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.teams.removeMemberError', 'Failed to remove user from team'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -163,12 +162,12 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio await userManagementService.deleteUser(user.username); alert({ alertType: 'success', title: t('workspace.people.deleteUserSuccess', 'User deleted successfully') }); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to delete user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.deleteUserError', 'Failed to delete user'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || + t('workspace.people.deleteUserError', 'Failed to delete user'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -185,12 +184,11 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio await userManagementService.unlockUser(user.username); alert({ alertType: 'success', title: t('workspace.people.unlockUserSuccess', 'User account unlocked successfully') }); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('[TeamDetailsSection] Failed to unlock user:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.people.unlockUserError', 'Failed to unlock user account'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.people.unlockUserError', 'Failed to unlock user account'); alert({ alertType: 'error', title: errorMessage }); } }; @@ -225,12 +223,12 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio setSelectedUser(null); setSelectedTeamId(''); fetchTeamDetails(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to change team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.changeTeam.error', 'Failed to change team'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || + t('workspace.teams.changeTeam.error', 'Failed to change team'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); diff --git a/frontend/src/proprietary/components/shared/config/configSections/TeamsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/TeamsSection.tsx index b2b9b986ae..ecaf058067 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/TeamsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/TeamsSection.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; import { Stack, @@ -83,12 +84,11 @@ export default function TeamsSection() { setNewTeamName(''); setCreateModalOpened(false); await fetchTeams(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to create team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.createTeam.error'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.teams.createTeam.error'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -109,12 +109,11 @@ export default function TeamsSection() { setSelectedTeam(null); setRenameModalOpened(false); await fetchTeams(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to rename team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.renameTeam.error'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || t('workspace.teams.renameTeam.error'); alert({ alertType: 'error', title: errorMessage }); } finally { setProcessing(false); @@ -135,12 +134,12 @@ export default function TeamsSection() { await teamService.deleteTeam(team.id); alert({ alertType: 'success', title: t('workspace.teams.deleteTeam.success') }); await fetchTeams(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to delete team:', error); - const errorMessage = error.response?.data?.message || - error.response?.data?.error || - error.message || - t('workspace.teams.deleteTeam.error'); + const errorMessage = isAxiosError(error) + ? (error.response?.data?.message || error.response?.data?.error || error.message) + : (error instanceof Error ? error.message : undefined) || + t('workspace.teams.deleteTeam.error'); alert({ alertType: 'error', title: errorMessage }); } }; diff --git a/frontend/src/proprietary/components/shared/config/configSections/audit/AuditEventsTable.tsx b/frontend/src/proprietary/components/shared/config/configSections/audit/AuditEventsTable.tsx index ba22d37221..f1e60bd02a 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/audit/AuditEventsTable.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/audit/AuditEventsTable.tsx @@ -15,7 +15,7 @@ import { UnstyledButton, } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import auditService, { AuditEvent } from '@app/services/auditService'; +import auditService, { AuditEvent, AuditFilters } from '@app/services/auditService'; import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; import { useAuditFilters } from '@app/hooks/useAuditFilters'; import AuditFiltersForm from '@app/components/shared/config/configSections/audit/AuditFiltersForm'; @@ -122,7 +122,7 @@ const AuditEventsTable: React.FC = ({ }, [filters, currentPage, loginEnabled]); // Wrap filter handlers to reset pagination - const handleFilterChangeWithReset = (key: keyof typeof filters, value: any) => { + const handleFilterChangeWithReset = (key: keyof AuditFilters, value: AuditFilters[keyof AuditFilters]) => { handleFilterChange(key, value); setCurrentPage(1); }; @@ -170,8 +170,8 @@ const AuditEventsTable: React.FC = ({ // Apply sorting to current events const sortedEvents = [...events].sort((a, b) => { - let aVal: any; - let bVal: any; + let aVal: string | number | undefined; + let bVal: string | number | undefined; switch (sortKey) { case 'timestamp': @@ -296,14 +296,14 @@ const AuditEventsTable: React.FC = ({ let author = ''; let fileHash = ''; if (event.details && typeof event.details === 'object') { - const details = event.details as Record; + const details = event.details as Record; const files = details.files; if (Array.isArray(files) && files.length > 0) { - const firstFile = files[0] as Record; - documentName = firstFile.name || ''; + const firstFile = files[0] as Record; + documentName = typeof firstFile.name === 'string' ? firstFile.name : ''; if (showAuthor || showFileHash) { - author = firstFile.pdfAuthor || ''; - fileHash = firstFile.fileHash ? firstFile.fileHash.substring(0, 16) + '...' : ''; + author = typeof firstFile.pdfAuthor === 'string' ? firstFile.pdfAuthor : ''; + fileHash = typeof firstFile.fileHash === 'string' ? firstFile.fileHash.substring(0, 16) + '...' : ''; } } } diff --git a/frontend/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.tsx index 53f6c53b59..5852c2f1ef 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/audit/AuditExportSection.tsx @@ -51,7 +51,7 @@ const AuditExportSection: React.FC = ({ try { setExporting(true); - const fieldsParam = Object.keys(selectedFields).filter(k => selectedFields[k as keyof typeof selectedFields]).join(','); + const fieldsParam = Object.keys(selectedFields).filter(k => selectedFields[k]).join(','); const blob = await auditService.exportData(exportFormat, { ...filters, fields: fieldsParam }); diff --git a/frontend/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.tsx b/frontend/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.tsx index 13c02081b2..f04e36c287 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/audit/AuditFiltersForm.tsx @@ -43,7 +43,7 @@ interface AuditFiltersFormProps { filters: AuditFilters; eventTypes: string[]; users: string[]; - onFilterChange: (key: keyof AuditFilters, value: any) => void; + onFilterChange: (key: keyof AuditFilters, value: AuditFilters[keyof AuditFilters]) => void; onClearFilters: () => void; disabled?: boolean; } @@ -147,8 +147,8 @@ const AuditFiltersForm: React.FC = ({ { - onFilterChange('startDate', value ? formatDateToYMD(value as Date) : undefined); + onChange={(value) => { + onFilterChange('startDate', value ? formatDateToYMD(new Date(value)) : undefined); }} clearable disabled={disabled} @@ -157,8 +157,8 @@ const AuditFiltersForm: React.FC = ({ { - onFilterChange('endDate', value ? formatDateToYMD(value as Date) : undefined); + onChange={(value) => { + onFilterChange('endDate', value ? formatDateToYMD(new Date(value)) : undefined); }} clearable disabled={disabled} diff --git a/frontend/src/proprietary/components/shared/stripeCheckout/hooks/useLicensePolling.ts b/frontend/src/proprietary/components/shared/stripeCheckout/hooks/useLicensePolling.ts index 0978683f3a..d9f23b6689 100644 --- a/frontend/src/proprietary/components/shared/stripeCheckout/hooks/useLicensePolling.ts +++ b/frontend/src/proprietary/components/shared/stripeCheckout/hooks/useLicensePolling.ts @@ -14,7 +14,7 @@ export const useLicensePolling = ( const pollForLicenseKey = useCallback(async (installId: string) => { // Use shared polling utility const result = await pollLicenseKeyWithBackoff(installId, { - isMounted: () => isMountedRef.current!, + isMounted: () => isMountedRef.current ?? false, onStatusChange: setPollingStatus, }); @@ -23,7 +23,7 @@ export const useLicensePolling = ( // Activate the license key const activation = await activateLicenseKey(result.licenseKey, { - isMounted: () => isMountedRef.current!, + isMounted: () => isMountedRef.current ?? false, onActivated: onLicenseActivated, }); diff --git a/frontend/src/proprietary/components/workflow/ParticipantView.tsx b/frontend/src/proprietary/components/workflow/ParticipantView.tsx index 6717b88203..9c011d68fd 100644 --- a/frontend/src/proprietary/components/workflow/ParticipantView.tsx +++ b/frontend/src/proprietary/components/workflow/ParticipantView.tsx @@ -112,8 +112,8 @@ const ParticipantView: React.FC = ({ token }) => { showLogo: true, }); setNotification({ type: 'success', message: 'Signature submitted successfully!' }); - } catch (err: any) { - setNotification({ type: 'error', message: `Failed to submit signature: ${err.message}` }); + } catch (err: unknown) { + setNotification({ type: 'error', message: `Failed to submit signature: ${err instanceof Error ? err.message : String(err)}` }); } finally { setIsSubmitting(false); } @@ -125,8 +125,8 @@ const ParticipantView: React.FC = ({ token }) => { try { await decline(token, declineReason || 'Declined by participant'); setNotification({ type: 'success', message: 'You have declined this signing request.' }); - } catch (err: any) { - setNotification({ type: 'error', message: `Failed to decline: ${err.message}` }); + } catch (err: unknown) { + setNotification({ type: 'error', message: `Failed to decline: ${err instanceof Error ? err.message : String(err)}` }); } } }; diff --git a/frontend/src/proprietary/contexts/ServerExperienceContext.tsx b/frontend/src/proprietary/contexts/ServerExperienceContext.tsx index edbf900773..fedb276183 100644 --- a/frontend/src/proprietary/contexts/ServerExperienceContext.tsx +++ b/frontend/src/proprietary/contexts/ServerExperienceContext.tsx @@ -8,6 +8,7 @@ import { type ReactNode, } from 'react'; import apiClient from '@app/services/apiClient'; +import { isAxiosError } from 'axios'; import { useAppConfig } from '@app/contexts/AppConfigContext'; import { useAuth } from '@app/auth/UseSession'; import { useLicense } from '@app/contexts/LicenseContext'; @@ -95,13 +96,8 @@ function getErrorMessage(error: unknown): string { if (typeof error === 'string') { return error; } - if ( - typeof error === 'object' && - error !== null && - 'response' in error && - typeof (error as any).response?.data?.message === 'string' - ) { - return (error as any).response.data.message; + if (isAxiosError(error) && typeof error.response?.data?.message === 'string') { + return error.response.data.message; } if (error instanceof Error) { return error.message; @@ -196,7 +192,7 @@ export function ServerExperienceProvider({ children }: { children: ReactNode }) ( await apiClient.get<{ totalUsers?: number }>( '/api/v1/proprietary/ui-data/admin-settings', - { suppressErrorToast: true } as any, + { suppressErrorToast: true }, ) ).data; const totalUsers = @@ -219,7 +215,7 @@ export function ServerExperienceProvider({ children }: { children: ReactNode }) ( await apiClient.get('/api/v1/info/wau', { suppressErrorToast: true, - } as any) + }) ).data; const weeklyActiveUsers = typeof responseData?.weeklyActiveUsers === 'number' diff --git a/frontend/src/proprietary/hooks/workflow/useParticipantSession.ts b/frontend/src/proprietary/hooks/workflow/useParticipantSession.ts index e150fc2332..fe47a457ba 100644 --- a/frontend/src/proprietary/hooks/workflow/useParticipantSession.ts +++ b/frontend/src/proprietary/hooks/workflow/useParticipantSession.ts @@ -1,4 +1,5 @@ import { useState, useCallback, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import workflowService, { WorkflowSessionResponse, ParticipantResponse, @@ -35,9 +36,10 @@ export const useParticipantSession = (token?: string): UseParticipantSessionResu ]); setSession(sessionData); setParticipant(participantData); - } catch (err: any) { - const errorMsg = - err.response?.data?.message || err.message || 'Failed to load session'; + } catch (err: unknown) { + const errorMsg = isAxiosError(err) + ? (err.response?.data?.message || err.message) + : (err instanceof Error ? err.message : undefined) || 'Failed to load session'; setError(errorMsg); } finally { setLoading(false); @@ -55,9 +57,10 @@ export const useParticipantSession = (token?: string): UseParticipantSessionResu if (request.participantToken) { await loadSession(request.participantToken); } - } catch (err: any) { - const errorMsg = - err.response?.data?.message || err.message || 'Failed to submit signature'; + } catch (err: unknown) { + const errorMsg = isAxiosError(err) + ? (err.response?.data?.message || err.message) + : (err instanceof Error ? err.message : undefined) || 'Failed to submit signature'; setError(errorMsg); throw new Error(errorMsg, { cause: err }); } finally { @@ -79,9 +82,10 @@ export const useParticipantSession = (token?: string): UseParticipantSessionResu setParticipant(updatedParticipant); // Reload session await loadSession(token); - } catch (err: any) { - const errorMsg = - err.response?.data?.message || err.message || 'Failed to decline'; + } catch (err: unknown) { + const errorMsg = isAxiosError(err) + ? (err.response?.data?.message || err.message) + : (err instanceof Error ? err.message : undefined) || 'Failed to decline'; setError(errorMsg); throw new Error(errorMsg, { cause: err }); } finally { @@ -104,9 +108,10 @@ export const useParticipantSession = (token?: string): UseParticipantSessionResu a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); - } catch (err: any) { - const errorMsg = - err.response?.data?.message || err.message || 'Failed to download document'; + } catch (err: unknown) { + const errorMsg = isAxiosError(err) + ? (err.response?.data?.message || err.message) + : (err instanceof Error ? err.message : undefined) || 'Failed to download document'; setError(errorMsg); } finally { setLoading(false); diff --git a/frontend/src/proprietary/routes/InviteAccept.tsx b/frontend/src/proprietary/routes/InviteAccept.tsx index c145309cab..9f174617b1 100644 --- a/frontend/src/proprietary/routes/InviteAccept.tsx +++ b/frontend/src/proprietary/routes/InviteAccept.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Stack, Text, Paper, Center, Loader, TextInput, PasswordInput, Anchor } from '@mantine/core'; @@ -56,14 +57,14 @@ export default function InviteAccept() { setLoading(true); const response = await apiClient.get(`/api/v1/invite/validate/${token}`, { suppressErrorToast: true, - } as any); + }); setInviteData(response.data); setError(null); - } catch (err: any) { - const errorMessage = - err.response?.data?.error || - err.message || - t('invite.validationError', 'Failed to validate invitation link'); + } catch (err: unknown) { + const errorMessage = isAxiosError(err) + ? (err.response?.data?.error || err.message) + : (err instanceof Error ? err.message : undefined) || + t('invite.validationError', 'Failed to validate invitation link'); setError(errorMessage); } finally { setLoading(false); @@ -108,15 +109,15 @@ export default function InviteAccept() { await apiClient.post(`/api/v1/invite/accept/${token}`, formData, { suppressErrorToast: true, - } as any); + }); // Success - redirect to login navigate('/login?messageType=accountCreated'); - } catch (err: any) { - const errorMessage = - err.response?.data?.error || - err.message || - t('invite.acceptError', 'Failed to create account'); + } catch (err: unknown) { + const errorMessage = isAxiosError(err) + ? (err.response?.data?.error || err.message) + : (err instanceof Error ? err.message : undefined) || + t('invite.acceptError', 'Failed to create account'); setError(errorMessage); } finally { setSubmitting(false); diff --git a/frontend/src/proprietary/routes/ShareLinkLoader.tsx b/frontend/src/proprietary/routes/ShareLinkLoader.tsx index 54bfbd812f..cb660082ea 100644 --- a/frontend/src/proprietary/routes/ShareLinkLoader.tsx +++ b/frontend/src/proprietary/routes/ShareLinkLoader.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef } from 'react'; +import { isAxiosError } from 'axios'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import apiClient from '@app/services/apiClient'; @@ -99,7 +100,7 @@ export default function ShareLinkLoader({ token }: ShareLinkLoaderProps) { const idMap = new Map(); for (let i = 0; i < stirlingFiles.length; i += 1) { - idMap.set(sortedEntries[i].logicalId, stirlingFiles[i].fileId as FileId); + idMap.set(sortedEntries[i].logicalId, stirlingFiles[i].fileId); } const rootIdMap = new Map(); @@ -199,9 +200,9 @@ export default function ShareLinkLoader({ token }: ShareLinkLoaderProps) { navActions.setWorkbench('viewer'); navigate('/', { replace: true }); - } catch (error: any) { + } catch (error: unknown) { if (signal.aborted) return; - const status = error?.response?.status; + const status = isAxiosError(error) ? error.response?.status : undefined; if (status === 401 || status === 403) { if (!isAuthenticated && !authLoading) { alert({ diff --git a/frontend/src/proprietary/routes/ShareLinkPage.tsx b/frontend/src/proprietary/routes/ShareLinkPage.tsx index 04f1060aeb..71d13c97cf 100644 --- a/frontend/src/proprietary/routes/ShareLinkPage.tsx +++ b/frontend/src/proprietary/routes/ShareLinkPage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { isAxiosError } from 'axios'; import { useNavigate, useParams } from 'react-router-dom'; import { Alert, Badge, Button, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core'; import { useTranslation } from 'react-i18next'; @@ -45,8 +46,8 @@ export default function ShareLinkPage() { const data = await fetchShareLinkMetadata(normalizedToken); setMetadata(data); setStatus('ready'); - } catch (error: any) { - const statusCode = error?.response?.status as number | undefined; + } catch (error: unknown) { + const statusCode = isAxiosError(error) ? error.response?.status : undefined; if (statusCode === 401) { setStatus('login'); } else if (statusCode === 403) { @@ -83,8 +84,8 @@ export default function ShareLinkPage() { link.click(); link.remove(); URL.revokeObjectURL(url); - } catch (error: any) { - const statusCode = error?.response?.status as number | undefined; + } catch (error: unknown) { + const statusCode = isAxiosError(error) ? error.response?.status : undefined; if (statusCode === 401) { setStatus('login'); } else if (statusCode === 403) { @@ -118,8 +119,8 @@ export default function ShareLinkPage() { } navActions.setWorkbench('viewer'); navigate('/', { replace: true }); - } catch (error: any) { - const statusCode = error?.response?.status as number | undefined; + } catch (error: unknown) { + const statusCode = isAxiosError(error) ? error.response?.status : undefined; if (statusCode === 401) { setStatus('login'); } else if (statusCode === 403) { diff --git a/frontend/src/proprietary/services/shareLinkImport.ts b/frontend/src/proprietary/services/shareLinkImport.ts index 6212ed72ec..b7da1b14e9 100644 --- a/frontend/src/proprietary/services/shareLinkImport.ts +++ b/frontend/src/proprietary/services/shareLinkImport.ts @@ -24,7 +24,7 @@ export interface ShareLinkMetadata { export async function fetchShareLinkMetadata(token: string): Promise { const response = await apiClient.get( `/api/v1/storage/share-links/${token}/metadata`, - { suppressErrorToast: true, skipAuthRedirect: true } as any + { suppressErrorToast: true, skipAuthRedirect: true } ); return response.data || {}; } @@ -38,7 +38,7 @@ export async function downloadShareLink(token: string): Promise<{ responseType: 'blob', suppressErrorToast: true, skipAuthRedirect: true, - } as any); + }); const contentType = (response.headers && (response.headers['content-type'] || response.headers['Content-Type'])) || ''; @@ -73,7 +73,7 @@ export async function importShareLinkToWorkbench( const idMap = new Map(); for (let i = 0; i < stirlingFiles.length; i += 1) { - idMap.set(sortedEntries[i].logicalId, stirlingFiles[i].fileId as FileId); + idMap.set(sortedEntries[i].logicalId, stirlingFiles[i].fileId); } const rootIdMap = new Map(); @@ -141,7 +141,7 @@ export async function importShareLinkToWorkbench( autoUnzip: false, skipAutoUnzip: false, }); - const ids = stirlingFiles.map((stirlingFile: StirlingFile) => stirlingFile.fileId as FileId); + const ids = stirlingFiles.map((stirlingFile: StirlingFile) => stirlingFile.fileId); if (ids.length > 0) { const sharedUpdates = { remoteStorageId: shareMetadata?.fileId, diff --git a/frontend/src/proprietary/services/teamService.ts b/frontend/src/proprietary/services/teamService.ts index 36434a2931..659fe4c7b3 100644 --- a/frontend/src/proprietary/services/teamService.ts +++ b/frontend/src/proprietary/services/teamService.ts @@ -1,4 +1,5 @@ import apiClient from '@app/services/apiClient'; +import type { User } from '@app/services/userManagementService'; export interface Team { id: number; @@ -25,6 +26,13 @@ export interface TeamDetailsResponse { availableUsers: TeamMember[]; } +export interface TeamDetailsUIResponse { + team: Team; + teamUsers: User[]; + availableUsers: User[]; + userLastRequest?: Record; +} + /** * Team Management Service * Provides functions to interact with team-related backend APIs @@ -41,8 +49,8 @@ export const teamService = { /** * Get team details including members */ - async getTeamDetails(teamId: number): Promise { - const response = await apiClient.get(`/api/v1/proprietary/ui-data/teams/${teamId}`); + async getTeamDetails(teamId: number): Promise { + const response = await apiClient.get(`/api/v1/proprietary/ui-data/teams/${teamId}`); return response.data; }, @@ -54,7 +62,7 @@ export const teamService = { formData.append('name', name); await apiClient.post('/api/v1/team/create', formData, { suppressErrorToast: true, - } as any); + }); }, /** @@ -66,7 +74,7 @@ export const teamService = { formData.append('newName', newName); await apiClient.post('/api/v1/team/rename', formData, { suppressErrorToast: true, - } as any); + }); }, /** @@ -77,7 +85,7 @@ export const teamService = { formData.append('teamId', teamId.toString()); await apiClient.post('/api/v1/team/delete', formData, { suppressErrorToast: true, - } as any); + }); }, /** @@ -89,7 +97,7 @@ export const teamService = { formData.append('userId', userId.toString()); await apiClient.post('/api/v1/team/addUser', formData, { suppressErrorToast: true, - } as any); + }); }, /** @@ -102,6 +110,6 @@ export const teamService = { formData.append('teamId', teamId.toString()); await apiClient.post('/api/v1/user/admin/changeRole', formData, { suppressErrorToast: true, - } as any); + }); }, }; diff --git a/frontend/src/proprietary/services/userManagementService.ts b/frontend/src/proprietary/services/userManagementService.ts index 0dafb670dd..09ff86983c 100644 --- a/frontend/src/proprietary/services/userManagementService.ts +++ b/frontend/src/proprietary/services/userManagementService.ts @@ -30,7 +30,7 @@ export interface AdminSettingsData { disabledUsers: number; currentUsername?: string; roleDetails?: Record; - teams?: any[]; + teams?: unknown[]; maxPaidUsers?: number; // License information maxAllowedUsers: number; @@ -39,7 +39,7 @@ export interface AdminSettingsData { licenseMaxUsers: number; premiumEnabled: boolean; mailEnabled: boolean; - userSettings?: Record; + userSettings?: Record; lockedUsers?: string[]; } @@ -155,7 +155,7 @@ export const userManagementService = { } await apiClient.post('/api/v1/user/admin/saveUser', formData, { suppressErrorToast: true, // Component will handle error display - } as any); + }); }, /** @@ -170,7 +170,7 @@ export const userManagementService = { } await apiClient.post('/api/v1/user/admin/changeRole', formData, { suppressErrorToast: true, - } as any); + }); }, /** @@ -181,7 +181,7 @@ export const userManagementService = { formData.append('enabled', enabled.toString()); await apiClient.post(`/api/v1/user/admin/changeUserEnabled/${username}`, formData, { suppressErrorToast: true, - } as any); + }); }, /** @@ -190,7 +190,7 @@ export const userManagementService = { async deleteUser(username: string): Promise { await apiClient.post(`/api/v1/user/admin/deleteUser/${username}`, null, { suppressErrorToast: true, - } as any); + }); }, /** @@ -211,7 +211,7 @@ export const userManagementService = { formData, { suppressErrorToast: true, // Component will handle error display - } as any + } ); return response.data; @@ -245,7 +245,7 @@ export const userManagementService = { formData, { suppressErrorToast: true, - } as any + } ); return response.data; @@ -265,7 +265,7 @@ export const userManagementService = { async revokeInviteLink(inviteId: number): Promise { await apiClient.delete(`/api/v1/invite/revoke/${inviteId}`, { suppressErrorToast: true, - } as any); + }); }, /** @@ -300,7 +300,7 @@ export const userManagementService = { await apiClient.post('/api/v1/user/admin/changePasswordForUser', formData, { suppressErrorToast: true, // Component will handle error display - } as any); + }); }, /** @@ -309,7 +309,7 @@ export const userManagementService = { async unlockUser(username: string): Promise { await apiClient.post(`/api/v1/user/admin/unlockUser/${username}`, null, { suppressErrorToast: true, - } as any); + }); }, /** From d68e6b6a291e18668518b8d3093a379e4b79795c Mon Sep 17 00:00:00 2001 From: Matheus Saito <106726276+MattSaito@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:48:53 -0300 Subject: [PATCH 02/59] Added back ctrl+r as rotate if on desktop (#5982) (#5993) Fix #5982 Behaviour of ctrl+r altered to support rotate on desktop, while the web version continue to use refresh as default. --- .../core/components/viewer/EmbedPdfViewer.tsx | 24 ++++++++++++------- .../src/core/hooks/useViewerKeyCommand.ts | 4 ++++ .../src/desktop/hooks/useViewerKeyCommand.ts | 20 ++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 frontend/src/core/hooks/useViewerKeyCommand.ts create mode 100644 frontend/src/desktop/hooks/useViewerKeyCommand.ts diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index 6d525be5c4..33a6a80f17 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -25,6 +25,7 @@ import type { PDFDict, PDFNumber } from '@cantoo/pdf-lib'; import { useWheelZoom } from '@app/hooks/useWheelZoom'; import { useFormFill } from '@app/tools/formFill/FormFillContext'; import { FormSaveBar } from '@app/tools/formFill/FormSaveBar'; +import { useViewerKeyCommand } from '@app/hooks/useViewerKeyCommand'; // ─── Measure dictionary extraction ──────────────────────────────────────────── @@ -380,15 +381,19 @@ const EmbedPdfViewerContent = ({ onZoomOut: zoomActions.zoomOut, }); + const viewerKeyCommand = useViewerKeyCommand(); + // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const mod = event.ctrlKey || event.metaKey; - // Ctrl+P (print) and Ctrl+R (rotate) must be intercepted unconditionally + // Ctrl+P (print) must be intercepted unconditionally // whenever the viewer is mounted, even before the user has hovered over it. + // Ctrl+R (rotate) is intercepted only on desktop (Tauri), while on web it still falls through to browser refresh. // Without this, the browser falls through to its native "print HTML page" // or "reload page" behaviour. + if (mod) { const target = event.target as Element; const isInTextInput = @@ -397,12 +402,15 @@ const EmbedPdfViewerContent = ({ (target as HTMLElement).isContentEditable; if (!isInTextInput) { - switch (event.key) { - case 'p': - case 'P': - event.preventDefault(); - printActions.print(); - return; + const wasOverridden = viewerKeyCommand(event) + if (!wasOverridden){ + switch (event.key) { + case 'p': + case 'P': + event.preventDefault(); + printActions.print(); + return; + } } } } @@ -509,7 +517,7 @@ const EmbedPdfViewerContent = ({ }, [ isViewerHovered, isSearchInterfaceVisible, zoomActions, searchInterfaceActions, scrollActions, printActions, exportActions, rotationActions, historyApiRef, - viewerApplyChanges, cyclePdfRenderMode, + viewerApplyChanges, cyclePdfRenderMode, viewerKeyCommand, ]); // Watch the annotation history API to detect when the document becomes "dirty". diff --git a/frontend/src/core/hooks/useViewerKeyCommand.ts b/frontend/src/core/hooks/useViewerKeyCommand.ts new file mode 100644 index 0000000000..d01546b679 --- /dev/null +++ b/frontend/src/core/hooks/useViewerKeyCommand.ts @@ -0,0 +1,4 @@ +// Default implementation for non-desktop environments (overridden in desktop) +export function useViewerKeyCommand(): (event: KeyboardEvent) => boolean { + return () => false; +} \ No newline at end of file diff --git a/frontend/src/desktop/hooks/useViewerKeyCommand.ts b/frontend/src/desktop/hooks/useViewerKeyCommand.ts new file mode 100644 index 0000000000..3a15af21e1 --- /dev/null +++ b/frontend/src/desktop/hooks/useViewerKeyCommand.ts @@ -0,0 +1,20 @@ +import { useViewer } from "@app/contexts/ViewerContext" +import { useCallback } from "react"; + +export function useViewerKeyCommand(): (event: KeyboardEvent) => boolean { + const { rotationActions } = useViewer(); + return useCallback((event:KeyboardEvent): boolean => { + switch (event.key) { + case 'r': + case 'R': + event.preventDefault(); + if (event.shiftKey) { + rotationActions.rotateBackward(); + } else { + rotationActions.rotateForward(); + } + return true; + } + return false; + }, [rotationActions]); +} \ No newline at end of file From 7058cc2a583f84ee9f66d13e6df7b54d3c106077 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:54:12 +0100 Subject: [PATCH 03/59] Remove gosu (#6036) --- .github/workflows/build.yml | 2 +- docker/base/Dockerfile | 5 ++--- docker/embedded/Dockerfile | 4 ++-- docker/embedded/Dockerfile.fat | 4 ++-- docker/embedded/Dockerfile.ultra-lite | 2 +- scripts/init-without-ocr.sh | 15 ++++----------- testing/test.sh | 16 ++++++++++++++-- 7 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5c8285d45..e431fd5f7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -513,7 +513,7 @@ jobs: echo "base_image=stirling-pdf-base:pr-test" >> $GITHUB_OUTPUT echo "platforms=linux/amd64" >> $GITHUB_OUTPUT else - echo "base_image=ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-base:latest" >> $GITHUB_OUTPUT + echo "base_image=stirlingtools/stirling-pdf-base:latest" >> $GITHUB_OUTPUT echo "platforms=linux/amd64,linux/arm64/v8" >> $GITHUB_OUTPUT fi diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index dfd64ee705..b2e8266851 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -386,7 +386,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # Core tools ca-certificates tzdata tini bash fontconfig curl \ ffmpeg poppler-utils fontforge \ - gosu unpaper pngquant \ + unpaper pngquant \ # Fonts: full coverage for standard + fat variants fonts-dejavu \ fonts-liberation2 \ @@ -622,8 +622,7 @@ RUN set -eux; \ -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \ || useradd -m -g stirlingpdfgroup \ -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \ - fi; \ - ln -sf /usr/sbin/gosu /usr/local/bin/su-exec + fi # Application directories RUN set -eux; \ diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index ee04dcf7e7..548294a315 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -1,8 +1,8 @@ # Stirling-PDF - Full version (embedded frontend) # Uses pre-built base image for fast builds -ARG BASE_VERSION=1.0.0 -ARG BASE_IMAGE=ghcr.io/stirling-tools/stirling-pdf-base:${BASE_VERSION} +ARG BASE_VERSION=1.0.1 +ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend FROM gradle:9.3.1-jdk25 AS app-build diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index dfdaaea691..766fede0f2 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -2,8 +2,8 @@ # Extra fonts for air-gapped environments # Uses pre-built base image for fast builds -ARG BASE_VERSION=1.0.0 -ARG BASE_IMAGE=ghcr.io/stirling-tools/stirling-pdf-base:${BASE_VERSION} +ARG BASE_VERSION=1.0.1 +ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend FROM gradle:9.3.1-jdk25 AS app-build diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index e0d719cd70..fb2e4bae56 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -93,7 +93,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a bash \ curl \ shadow \ - su-exec && \ + util-linux && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /tmp/stirling-pdf/heap_dumps && \ mkdir -p /usr/share/fonts/opentype/noto && \ # User permissions diff --git a/scripts/init-without-ocr.sh b/scripts/init-without-ocr.sh index 26e98c598c..c3f63a4124 100755 --- a/scripts/init-without-ocr.sh +++ b/scripts/init-without-ocr.sh @@ -176,13 +176,6 @@ UNOSERVER_PIDS=() UNOSERVER_PORTS=() UNOSERVER_UNO_PORTS=() -SU_EXEC_BIN="" -if command_exists su-exec; then - SU_EXEC_BIN="su-exec" -elif command_exists gosu; then - SU_EXEC_BIN="gosu" -fi - CURRENT_USER="$(id -un)" CURRENT_UID="$(id -u)" SWITCH_USER_WARNING_EMITTED=false @@ -197,8 +190,8 @@ warn_switch_user_once() { run_as_runtime_user() { if [ "$CURRENT_USER" = "$RUNTIME_USER" ]; then "$@" - elif [ "$CURRENT_UID" -eq 0 ] && [ -n "$SU_EXEC_BIN" ]; then - "$SU_EXEC_BIN" "$RUNTIME_USER" "$@" + elif [ "$CURRENT_UID" -eq 0 ] && command_exists setpriv; then + setpriv --reuid="$RUNTIME_USER" --regid="$(id -gn "$RUNTIME_USER")" --init-groups -- "$@" else warn_switch_user_once "$@" @@ -915,8 +908,8 @@ fi if [ "$CURRENT_USER" = "$RUNTIME_USER" ]; then "${JAVA_CMD[@]}" & -elif [ "$CURRENT_UID" -eq 0 ] && [ -n "$SU_EXEC_BIN" ]; then - "$SU_EXEC_BIN" "$RUNTIME_USER" "${JAVA_CMD[@]}" & +elif [ "$CURRENT_UID" -eq 0 ] && command_exists setpriv; then + setpriv --reuid="$RUNTIME_USER" --regid="$(id -gn "$RUNTIME_USER")" --init-groups -- "${JAVA_CMD[@]}" & else warn_switch_user_once "${JAVA_CMD[@]}" & diff --git a/testing/test.sh b/testing/test.sh index 9308cd71d3..4dce6c0157 100644 --- a/testing/test.sh +++ b/testing/test.sh @@ -88,6 +88,7 @@ capture_failure_logs() { capture_build_failure() { local build_name=$1 local log_file="$REPORT_DIR/${build_name//[^a-zA-Z0-9_-]/_}.failure.log" + local build_log="$REPORT_DIR/${build_name//[^a-zA-Z0-9_-]/_}.build.log" local gradle_report_dirs=( "$PROJECT_ROOT/app/core/build/reports/tests" "$PROJECT_ROOT/app/common/build/reports/tests" @@ -99,6 +100,13 @@ capture_build_failure() { echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "---" + # Include Docker/command build output if captured + if [ -f "$build_log" ]; then + echo "--- Build output (last 100 lines) ---" + tail -100 "$build_log" + echo "" + fi + for report_dir in "${gradle_report_dirs[@]}"; do if [ -d "$report_dir" ]; then local txt_index="$report_dir/test/index.html" @@ -699,12 +707,14 @@ main() { else DOCKER_CACHE_ARGS_ULTRA_LITE="" fi + local ultra_lite_build_log="$REPORT_DIR/Build-Ultra-Lite-Docker.build.log" if ! docker buildx build --build-arg VERSION_TAG=alpha \ -t docker.stirlingpdf.com/stirlingtools/stirling-pdf:ultra-lite \ -f ./docker/embedded/Dockerfile.ultra-lite \ --load \ - ${DOCKER_CACHE_ARGS_ULTRA_LITE} .; then + ${DOCKER_CACHE_ARGS_ULTRA_LITE} . 2>&1 | tee "$ultra_lite_build_log"; then failed_tests+=("Build-Ultra-Lite-Docker") + capture_build_failure "Build-Ultra-Lite-Docker" gha_endgroup exit 1 fi @@ -783,13 +793,15 @@ main() { else DOCKER_CACHE_ARGS_FAT="" fi + local fat_build_log="$REPORT_DIR/Build-Fat-Docker.build.log" if ! docker buildx build --build-arg VERSION_TAG=alpha \ ${BASE_IMAGE_ARG} \ -t docker.stirlingpdf.com/stirlingtools/stirling-pdf:fat \ -f ./docker/embedded/Dockerfile.fat \ --load \ - ${DOCKER_CACHE_ARGS_FAT} .; then + ${DOCKER_CACHE_ARGS_FAT} . 2>&1 | tee "$fat_build_log"; then failed_tests+=("Build-Fat-Docker") + capture_build_failure "Build-Fat-Docker" gha_endgroup exit 1 fi From e78cf0564b7fc890043b4b51dca7317135121cc3 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:54:33 +0100 Subject: [PATCH 04/59] qr split fixes (#6043) --- .../api/misc/AutoSplitPdfController.java | 335 +++++++++++++----- 1 file changed, 251 insertions(+), 84 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index eec08ba9b6..2de7de4ca9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -1,19 +1,22 @@ package stirling.software.SPDF.controller.api.misc; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; -import java.awt.image.DataBufferInt; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; -import java.util.HashSet; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -21,6 +24,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; import com.google.zxing.*; +import com.google.zxing.common.GlobalHistogramBinarizer; import com.google.zxing.common.HybridBinarizer; import io.github.pixee.security.Filenames; @@ -35,7 +39,6 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; @@ -48,61 +51,219 @@ import stirling.software.common.util.WebResponseUtils; public class AutoSplitPdfController { private static final Set VALID_QR_CONTENTS = - new HashSet<>( - Set.of( - "https://github.com/Stirling-Tools/Stirling-PDF", - "https://github.com/Frooodle/Stirling-PDF", - "https://stirlingpdf.com")); + Set.of( + "https://github.com/Stirling-Tools/Stirling-PDF", + "https://github.com/Frooodle/Stirling-PDF", + "https://stirlingpdf.com"); + + private static final int MAX_IMAGES_FOR_DIRECT_EXTRACTION = 3; + + // 150 DPI is sufficient for QR code detection — higher wastes memory and CPU + private static final int QR_DETECTION_DPI = 150; + + // Max total pixels before we downscale to avoid OOM on getRGB() allocation + private static final long MAX_IMAGE_PIXELS = 100_000_000L; // ~10000x10000 + + // Number of evenly-spaced pixel samples used for the blank image check + private static final int BLANK_CHECK_SAMPLES = 20; + + private static final Map DECODE_HINTS; + + static { + DECODE_HINTS = new EnumMap<>(DecodeHintType.class); + DECODE_HINTS.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); + DECODE_HINTS.put(DecodeHintType.ALSO_INVERTED, Boolean.TRUE); + DECODE_HINTS.put(DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE)); + } private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; + private final ApplicationProperties applicationProperties; - private static String decodeQRCode(BufferedImage bufferedImage) { - LuminanceSource source; - - if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte dataBufferByte) { - byte[] pixels = dataBufferByte.getData(); - source = - new PlanarYUVLuminanceSource( - pixels, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - 0, - 0, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - false); - } else if (bufferedImage.getRaster().getDataBuffer() - instanceof DataBufferInt dataBufferInt) { - int[] pixels = dataBufferInt.getData(); - byte[] newPixels = new byte[pixels.length]; - for (int i = 0; i < pixels.length; i++) { - newPixels[i] = (byte) (pixels[i] & 0xff); - } - source = - new PlanarYUVLuminanceSource( - newPixels, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - 0, - 0, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - false); - } else { - throw new IllegalArgumentException( - "BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed" - + " int), byte gray, or 3-byte/4-byte RGB image data"); + /** + * Downscale an image if it exceeds the maximum pixel count. Scales uniformly based on the + * pixel-count ratio so both portrait and landscape images are handled correctly. + */ + private static BufferedImage downscaleIfNeeded(BufferedImage image) { + long totalPixels = (long) image.getWidth() * image.getHeight(); + if (totalPixels <= MAX_IMAGE_PIXELS) { + return image; } + double scale = Math.sqrt((double) MAX_IMAGE_PIXELS / totalPixels); + int newWidth = Math.max(1, (int) (image.getWidth() * scale)); + int newHeight = Math.max(1, (int) (image.getHeight() * scale)); + log.debug( + "Downscaling image from {}x{} to {}x{} for QR detection", + image.getWidth(), + image.getHeight(), + newWidth, + newHeight); + BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = scaled.createGraphics(); + g.drawImage(image, 0, 0, newWidth, newHeight, null); + g.dispose(); + return scaled; + } - BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + /** + * Quick check whether an image appears to be blank (single solid colour). Samples pixels at + * evenly-spaced positions — if all samples match the first pixel the image is almost certainly + * blank (e.g. a masked image that returned solid white). + */ + private static boolean isBlankImage(int[] pixels) { + if (pixels.length == 0) return true; + int first = pixels[0]; + int step = Math.max(1, pixels.length / BLANK_CHECK_SAMPLES); + for (int i = step; i < pixels.length; i += step) { + if (pixels[i] != first) { + return false; + } + } + return true; + } + /** + * Try to decode a QR code from pre-extracted RGB pixel data using multiple binarization + * strategies. Returns the decoded text or null. + * + *

Strategy 1: HybridBinarizer — good for variable brightness (digital PDFs). + * + *

Strategy 2: GlobalHistogramBinarizer — better for scanned/noisy images with uniform + * lighting, and for QR codes with embedded logos that confuse the hybrid approach. + */ + private static String tryDecodeQR(int[] pixels, int width, int height) { + RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); + MultiFormatReader reader = new MultiFormatReader(); + + // Strategy 1: HybridBinarizer — good for variable brightness (digital PDFs) try { - Result result = new MultiFormatReader().decode(bitmap); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + Result result = reader.decode(bitmap, DECODE_HINTS); + log.debug("QR detected via HybridBinarizer: '{}'", result.getText()); return result.getText(); } catch (NotFoundException e) { - return null; // there is no QR code in the image + // continue } + + // Strategy 2: GlobalHistogramBinarizer — better for scanned/noisy images + try { + BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source)); + Result result = reader.decode(bitmap, DECODE_HINTS); + log.debug("QR detected via GlobalHistogramBinarizer: '{}'", result.getText()); + return result.getText(); + } catch (NotFoundException e) { + return null; + } + } + + /** + * Attempt to decode a QR code from a BufferedImage. Handles downscaling for oversized images + * and skips blank images early. + */ + private static String decodeQRCode(BufferedImage bufferedImage) { + bufferedImage = downscaleIfNeeded(bufferedImage); + + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + int[] pixels = new int[width * height]; + bufferedImage.getRGB(0, 0, width, height, pixels, 0, width); + + // Skip blank images early (e.g. masked images that decode to solid white) + if (isBlankImage(pixels)) { + log.debug("Skipping blank {}x{} image", width, height); + return null; + } + + return tryDecodeQR(pixels, width, height); + } + + /** Count the number of images embedded in a page's resources. */ + private static int countPageImages(PDPage page) { + if (page.getResources() == null || page.getResources().getXObjectNames() == null) { + return 0; + } + int count = 0; + for (COSName name : page.getResources().getXObjectNames()) { + if (page.getResources().isImageXObject(name)) { + count++; + } + } + return count; + } + + /** + * Extract images directly from a page's resources and check each for a QR code. Returns the QR + * code text if found, null otherwise. + */ + private static String checkPageImagesDirect(PDPage page) throws IOException { + if (page.getResources() == null || page.getResources().getXObjectNames() == null) { + return null; + } + for (COSName name : page.getResources().getXObjectNames()) { + if (!page.getResources().isImageXObject(name)) { + continue; + } + PDImageXObject imageObject = (PDImageXObject) page.getResources().getXObject(name); + + BufferedImage image; + try { + image = imageObject.getImage(); + } catch (OutOfMemoryError e) { + log.warn( + "Skipping oversized embedded image '{}' ({}x{}) - out of memory", + name.getName(), + imageObject.getWidth(), + imageObject.getHeight()); + continue; + } + + String result = decodeQRCode(image); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Render the full page to an image and scan it for a QR code. Tries a low DPI first (fast, low + * memory) and only retries at the system's maxDPI if detection fails. The first rendered image + * is released before the retry to allow GC to reclaim it. + */ + private String checkPageByRendering(PDFRenderer pdfRenderer, int pageNum) throws IOException { + log.debug("Rendering page {} at {} DPI for QR detection", pageNum + 1, QR_DETECTION_DPI); + + BufferedImage bim = + ExceptionUtils.handleOomRendering( + pageNum + 1, + QR_DETECTION_DPI, + () -> pdfRenderer.renderImageWithDPI(pageNum, QR_DETECTION_DPI)); + String result = decodeQRCode(bim); + bim = null; // allow GC before potential high-DPI retry + + if (result == null) { + int maxDpi = getSystemMaxDpi(); + if (maxDpi > QR_DETECTION_DPI) { + log.debug( + "Retrying page {} at {} DPI (low-DPI detection failed)", + pageNum + 1, + maxDpi); + BufferedImage highRes = + ExceptionUtils.handleOomRendering( + pageNum + 1, + maxDpi, + () -> pdfRenderer.renderImageWithDPI(pageNum, maxDpi)); + result = decodeQRCode(highRes); + } + } + return result; + } + + private int getSystemMaxDpi() { + if (applicationProperties != null && applicationProperties.getSystem() != null) { + return applicationProperties.getSystem().getMaxDPI(); + } + return QR_DETECTION_DPI; } @AutoJobPostMapping(value = "/auto-split-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -111,42 +272,56 @@ public class AutoSplitPdfController { summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and" - + " splits the document at the QR code boundaries. The output is a zip file" - + " containing each separate PDF document. Input:PDF Output:ZIP-PDF" + + " splits the document at the QR code boundaries. The output is a zip" + + " file containing each separate PDF document. Input:PDF Output:ZIP-PDF" + " Type:SISO") public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); boolean duplexMode = Boolean.TRUE.equals(request.getDuplexMode()); + log.info( + "Auto-split starting: filename='{}', size={} bytes, duplexMode={}", + file.getOriginalFilename(), + file.getSize(), + duplexMode); + List splitDocuments = new ArrayList<>(); try (TempFile outputTempFile = new TempFile(tempFileManager, ".zip"); PDDocument document = pdfDocumentFactory.load(file.getInputStream())) { + int totalPages = document.getNumberOfPages(); + log.info("PDF loaded, totalPages={}", totalPages); + PDFRenderer pdfRenderer = new PDFRenderer(document); pdfRenderer.setSubsamplingAllowed(true); - for (int page = 0; page < document.getNumberOfPages(); ++page) { - BufferedImage bim; + for (int page = 0; page < totalPages; ++page) { + PDPage pdPage = document.getPage(page); + int imageCount = countPageImages(pdPage); - // Use global maximum DPI setting, fallback to 300 if not set - int renderDpi = 150; // Default fallback - ApplicationProperties properties = - ApplicationContextProvider.getBean(ApplicationProperties.class); - if (properties != null && properties.getSystem() != null) { - renderDpi = properties.getSystem().getMaxDPI(); + String qrResult; + if (imageCount > 0 && imageCount <= MAX_IMAGES_FOR_DIRECT_EXTRACTION) { + // Try extracting images directly from the PDF (faster, avoids rendering) + qrResult = checkPageImagesDirect(pdPage); + if (qrResult == null) { + // Fall back to rendering — the image may use masking/compositing + // that getImage() doesn't resolve, or the QR may be vector-drawn + qrResult = checkPageByRendering(pdfRenderer, page); + } + } else { + // Too many images or no images — render the full page + qrResult = checkPageByRendering(pdfRenderer, page); } - final int dpi = renderDpi; - final int pageNum = page; - bim = - ExceptionUtils.handleOomRendering( - pageNum + 1, - dpi, - () -> pdfRenderer.renderImageWithDPI(pageNum, dpi)); - String result = decodeQRCode(bim); + boolean isValidQrCode = qrResult != null && VALID_QR_CONTENTS.contains(qrResult); + if (isValidQrCode) { + log.info( + "Page {}/{} contains QR divider ('{}')", + page + 1, + totalPages, + qrResult); + } - boolean isValidQrCode = VALID_QR_CONTENTS.contains(result); - log.debug("detected qr code {}, code is vale={}", result, isValidQrCode); if (isValidQrCode && page != 0) { splitDocuments.add(new PDDocument()); } @@ -159,32 +334,25 @@ public class AutoSplitPdfController { splitDocuments.add(firstDocument); } - // If duplexMode is true and current page is a divider, then skip next page if (duplexMode && isValidQrCode) { - page++; + page++; // skip back of divider page } } - // Remove split documents that have no pages splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0); + log.info("Split complete, {} output documents", splitDocuments.size()); String filename = GeneralUtils.removeExtension( Filenames.toSimpleFileName(file.getOriginalFilename())); - try (ZipOutputStream zipOut = - new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) { + // Stream split documents directly into zip — avoids holding all PDFs in memory + try (OutputStream fileOut = Files.newOutputStream(outputTempFile.getPath()); + ZipOutputStream zipOut = new ZipOutputStream(fileOut)) { for (int i = 0; i < splitDocuments.size(); i++) { String fileName = filename + "_" + (i + 1) + ".pdf"; - PDDocument splitDocument = splitDocuments.get(i); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - splitDocument.save(baos); - byte[] pdf = baos.toByteArray(); - - ZipEntry pdfEntry = new ZipEntry(fileName); - zipOut.putNextEntry(pdfEntry); - zipOut.write(pdf); + zipOut.putNextEntry(new ZipEntry(fileName)); + splitDocuments.get(i).save(zipOut); zipOut.closeEntry(); } } @@ -197,7 +365,6 @@ public class AutoSplitPdfController { log.error("Error in auto split", e); throw e; } finally { - // Clean up split documents for (PDDocument splitDoc : splitDocuments) { try { splitDoc.close(); From bcb4f3b13273611ad1015ca9c35f25a5e30b4168 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:58:10 +0100 Subject: [PATCH 05/59] idle cpu fix test (#6015) --- .../software/common/util/FileMonitor.java | 16 ++++++++++++---- .../src/core/components/viewer/LocalEmbedPDF.tsx | 5 ++++- scripts/init-without-ocr.sh | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/util/FileMonitor.java b/app/common/src/main/java/stirling/software/common/util/FileMonitor.java index 09caccdb50..5bdda6b5e2 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileMonitor.java +++ b/app/common/src/main/java/stirling/software/common/util/FileMonitor.java @@ -112,8 +112,6 @@ public class FileMonitor { All files observed changes in the last iteration will be considered as staging files. If those files are not modified in current iteration, they will be considered as ready for processing. */ - stagingFiles = new HashSet<>(newlyDiscoveredFiles); - readyForProcessingFiles.clear(); if (path2KeyMapping.isEmpty()) { log.warn("Not monitoring any directories; attempting to re-register root paths."); @@ -129,8 +127,17 @@ public class FileMonitor { } } - WatchKey key; - while ((key = watchService.poll()) != null) { + // Skip expensive collection work when there is nothing to track + WatchKey firstKey = watchService.poll(); + if (firstKey == null && newlyDiscoveredFiles.isEmpty() && readyForProcessingFiles.isEmpty()) { + return; + } + + stagingFiles = new HashSet<>(newlyDiscoveredFiles); + readyForProcessingFiles.clear(); + + WatchKey key = firstKey; + while (key != null) { final Path watchingDir = (Path) key.watchable(); key.pollEvents() .forEach( @@ -167,6 +174,7 @@ public class FileMonitor { if (!isKeyValid) { // key is invalid when the directory itself is no longer exists path2KeyMapping.remove((Path) key.watchable()); } + key = watchService.poll(); } readyForProcessingFiles.addAll(stagingFiles); } diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index 9050cf20e6..db3227d3b2 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -4,6 +4,7 @@ import type { PluginRegistry } from '@embedpdf/core'; import { EmbedPDF } from '@embedpdf/core/react'; import { usePdfiumEngine } from '@embedpdf/engines/react'; import { PrivateContent } from '@app/components/shared/PrivateContent'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; // Import the essential plugins import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react'; @@ -94,15 +95,17 @@ interface LocalEmbedPDFProps { export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId, isCommentsSidebarVisible = false, commentsSidebarRightOffset = '0rem', isSignMode = false, pdfRenderMode = 'normal' }: LocalEmbedPDFProps) { const { t } = useTranslation(); + const { config } = useAppConfig(); const [pdfUrl, setPdfUrl] = useState(null); const [, setAnnotations] = useState>([]); const [commentAuthorName, setCommentAuthorName] = useState('Guest'); useEffect(() => { + if (!config?.enableLogin) return; accountService.getAccountData().then((data) => { if (data?.username) setCommentAuthorName(data.username); }).catch(() => {/* not logged in or security disabled */}); - }, []); + }, [config?.enableLogin]); // Convert File to URL if needed useEffect(() => { diff --git a/scripts/init-without-ocr.sh b/scripts/init-without-ocr.sh index c3f63a4124..f7707fcc36 100755 --- a/scripts/init-without-ocr.sh +++ b/scripts/init-without-ocr.sh @@ -287,10 +287,20 @@ start_unoserver_watchdog() { if [ "$needs_restart" = true ]; then log "Restarting unoserver on 127.0.0.1:${port} (uno-port ${uno_port})" - # Kill the old process if it exists + # Kill the old process and its children (soffice) if it exists. + # Capture child PIDs first, then send TERM to children before parent + # so the PPID relationship is still visible. After sleep, use the + # saved PIDs for SIGKILL since the parent may have already exited + # and children would be reparented to init. if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + local child_pids + child_pids=$(pgrep -P "$pid" 2>/dev/null || true) + pkill -TERM -P "$pid" 2>/dev/null || true kill -TERM "$pid" 2>/dev/null || true - sleep 1 + sleep 3 + if [ -n "$child_pids" ]; then + kill -KILL $child_pids 2>/dev/null || true + fi kill -KILL "$pid" 2>/dev/null || true fi start_unoserver_instance "$port" "$uno_port" From c8296af41c99dc00a81375d96fd391f1c9b2eb68 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:58:38 +0100 Subject: [PATCH 06/59] fix new line in redact (#6035) --- .../security/configuration/SecurityConfiguration.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 956f91b7a2..bce3bf63e8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -159,10 +159,13 @@ public class SecurityConfiguration { firewall.setAllowedHeaderValues( headerValue -> headerValue != null && allowedChars.matcher(headerValue).matches()); - // Apply the same rules to parameter values for consistency. + // Allow non-ASCII characters and newlines in parameter values. + Pattern allowedParamChars = + Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]\\r\\n]*"); firewall.setAllowedParameterValues( parameterValue -> - parameterValue != null && allowedChars.matcher(parameterValue).matches()); + parameterValue != null + && allowedParamChars.matcher(parameterValue).matches()); return firewall; } From 9dcec10c06209b97aa73d7cec37a5ce5505c2044 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:33:46 +0100 Subject: [PATCH 07/59] Bug/connection mode fixes (#5998) --- frontend/config/.env.desktop.example | 2 +- .../public/locales/en-GB/translation.toml | 3 + frontend/src-tauri/src/commands/connection.rs | 27 +++++- .../src/desktop/components/AppProviders.tsx | 97 ++++++++++--------- .../components/DesktopOnboardingModal.tsx | 17 +--- .../desktop/components/SetupWizard/index.tsx | 49 +++++++--- .../shared/DisabledButtonWithTooltip.css | 38 ++++++++ .../shared/DisabledButtonWithTooltip.tsx | 36 +++++++ .../desktop/services/connectionModeService.ts | 32 +++--- .../proprietary/services/licenseService.ts | 2 +- 10 files changed, 211 insertions(+), 92 deletions(-) create mode 100644 frontend/src/desktop/components/shared/DisabledButtonWithTooltip.css create mode 100644 frontend/src/desktop/components/shared/DisabledButtonWithTooltip.tsx diff --git a/frontend/config/.env.desktop.example b/frontend/config/.env.desktop.example index 2e58bebec8..a83666a4e1 100644 --- a/frontend/config/.env.desktop.example +++ b/frontend/config/.env.desktop.example @@ -10,4 +10,4 @@ VITE_SAAS_BACKEND_API_URL=https://api2.stirling.com # Dev only: set to true to mimic an expired access token (no valid JWT for API/auth checks). # Production builds ignore this. Restart tauri-dev after changing. -VITE_DEV_SIMULATE_EXPIRED_JWT=false +VITE_DEV_SIMULATE_EXPIRED_JWT= diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index c35791ca69..cf99c8aee8 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -6817,10 +6817,13 @@ title = "Sign in to Stirling" [setup.selfhosted] link = "or connect to a self-hosted account" subtitle = "Enter your server credentials" +changeServerLocked = "Your organisation has restricted this app to a specific server" switchToLocal = "Use local tools instead" title = "Sign in to Server" [setup.selfhosted.unreachable] +changeServer = "Connect to a different server" +changeServerLocked = "Your organisation has restricted this app to a specific server" continueOffline = "Use local tools instead" message = "Could not reach {{url}}. Check that the server is running and accessible." retry = "Retry" diff --git a/frontend/src-tauri/src/commands/connection.rs b/frontend/src-tauri/src/commands/connection.rs index 8b8c68a814..157f25ce5b 100644 --- a/frontend/src-tauri/src/commands/connection.rs +++ b/frontend/src-tauri/src/commands/connection.rs @@ -72,6 +72,28 @@ pub async fn set_connection_mode( ) -> Result<(), String> { log::info!("Setting connection mode: {:?}", mode); + let store = app_handle + .store(STORE_FILE) + .map_err(|e| format!("Failed to access store: {}", e))?; + + // If the store is already locked, protect connection_mode, server_config, and the lock + // flag from being overwritten by any JS-side call. + // Only allow marking setup_completed and updating auth-related fields. + let already_locked = store + .get(LOCK_CONNECTION_KEY) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if already_locked { + log::warn!("set_connection_mode called while lock_connection_mode=true — preserving connection settings, but marking setup as completed"); + // Still allow setup_completed to be written so the onboarding doesn't repeat. + store.set(FIRST_LAUNCH_KEY, serde_json::json!(true)); + store + .save() + .map_err(|e| format!("Failed to save store: {}", e))?; + return Ok(()); + } + // Update in-memory state if let Ok(mut conn_state) = state.0.lock() { conn_state.mode = mode.clone(); @@ -81,11 +103,6 @@ pub async fn set_connection_mode( } } - // Save to store - let store = app_handle - .store(STORE_FILE) - .map_err(|e| format!("Failed to access store: {}", e))?; - store.set( CONNECTION_MODE_KEY, serde_json::to_value(&mode).map_err(|e| format!("Failed to serialize mode: {}", e))?, diff --git a/frontend/src/desktop/components/AppProviders.tsx b/frontend/src/desktop/components/AppProviders.tsx index 974d25a5f9..92f2e63911 100644 --- a/frontend/src/desktop/components/AppProviders.tsx +++ b/frontend/src/desktop/components/AppProviders.tsx @@ -10,7 +10,7 @@ import { ToolActionsContext } from '@app/contexts/ToolActionsContext'; import { useFirstLaunchCheck } from '@app/hooks/useFirstLaunchCheck'; import { useBackendInitializer } from '@app/hooks/useBackendInitializer'; import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig'; -import { connectionModeService } from '@app/services/connectionModeService'; +import { connectionModeService, JWT_EXPIRED_PROMPTED_KEY } from '@app/services/connectionModeService'; import { STIRLING_SAAS_URL } from '@app/constants/connection'; import { tauriBackendService } from '@app/services/tauriBackendService'; import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor'; @@ -47,9 +47,7 @@ export function AppProviders({ children }: { children: ReactNode }) { const { isFirstLaunch, setupComplete } = useFirstLaunchCheck(); const [connectionMode, setConnectionMode] = useState<'saas' | 'selfhosted' | 'local' | null>(null); const [authChecked, setAuthChecked] = useState(false); - // When auth check finds no valid session, record the sign-in detail here so the - // dispatch useEffect below can fire it only after SignInModal has mounted. - const [pendingSignIn, setPendingSignIn] = useState<{ locked: boolean } | null>(null); + const [pendingSignIn, setPendingSignIn] = useState(false); // Prevent first-launch setup from running twice when connectionMode state update re-triggers the effect const firstLaunchInitiated = useRef(false); // Key incremented on every connection mode change after initial load — forces SaaS provider @@ -99,69 +97,73 @@ export function AppProviders({ children }: { children: ReactNode }) { }) .finally(() => setAuthChecked(true)); } else { - let pendingDetail: { locked: boolean } | null = null; authService.isAuthenticated() .then(async (isAuth) => { if (!isAuth) { const cfg = await connectionModeService.getCurrentConfig().catch(() => null); - if (cfg?.lock_connection_mode) { - // Provisioned deployment — stay in the configured mode and prompt for credentials. - // Don't fall back to local; the admin has locked the connection mode. - pendingDetail = { locked: true }; - } else { - // JWT expired — fall back to local so local tools still work, then prompt - // for re-authentication via the sign-in modal. + if (!cfg?.lock_connection_mode) { + // JWT expired — fall back to local so local tools still work. await connectionModeService.switchToLocal().catch(console.error); setConnectionMode('local'); - pendingDetail = { locked: false }; + // Show sign-in modal once per expiry cycle. If the user dismisses + // without signing in the flag stays set and we won't prompt again + // until they successfully sign in (which clears the flag). + if (!localStorage.getItem(JWT_EXPIRED_PROMPTED_KEY)) { + localStorage.setItem(JWT_EXPIRED_PROMPTED_KEY, 'true'); + setPendingSignIn(true); + } } + // Locked deployments stay in their configured mode — user can sign in + // via Settings when they're ready. } }) .catch(async () => { const cfg = await connectionModeService.getCurrentConfig().catch(() => null); - if (cfg?.lock_connection_mode) { - // Auth check threw (e.g. network error) but mode is locked — still prompt for - // credentials so the user can sign in when connectivity is restored. - pendingDetail = { locked: true }; - } else { + if (!cfg?.lock_connection_mode) { await connectionModeService.switchToLocal().catch(console.error); setConnectionMode('local'); - pendingDetail = { locked: false }; + if (!localStorage.getItem(JWT_EXPIRED_PROMPTED_KEY)) { + localStorage.setItem(JWT_EXPIRED_PROMPTED_KEY, 'true'); + setPendingSignIn(true); + } } }) - .finally(() => { - setAuthChecked(true); - // Schedule sign-in via state so the dispatch useEffect fires AFTER - // SignInModal mounts (children effects run before parent effects). - if (pendingDetail) { - setPendingSignIn(pendingDetail); - } - }); + .finally(() => setAuthChecked(true)); } } else if (isFirstLaunch && !setupComplete) { - // Auto-enter local mode on first launch — skip the setup wizard entirely. - // The onboarding carousel + sign-in toast will be shown inside the main app. - // Start the backend explicitly here because shouldMonitorBackend relies on - // setupComplete (still false from the hook), so useBackendInitializer won't fire. - // Guard against re-running when setConnectionMode('local') below triggers this effect. + // Guard against re-running when setConnectionMode triggers this effect. if (firstLaunchInitiated.current) return; firstLaunchInitiated.current = true; - connectionModeService.switchToLocal() - .then(() => tauriBackendService.startBackend()) + connectionModeService.getCurrentConfig() + .then(async (cfg) => { + if (cfg.lock_connection_mode && cfg.server_config?.url) { + // Locked provisioned deployment — do NOT switch to local (would clear server_config + // from the store). Show onboarding normally; the sign-in slide handles locked auth. + // Still start the local backend so local tools work while the user signs in. + await tauriBackendService.startBackend().catch(console.error); + setConnectionMode('selfhosted'); + } else { + // Normal first launch — auto-enter local mode. + // The onboarding carousel + sign-in slide will be shown inside the main app. + await connectionModeService.switchToLocal(); + await tauriBackendService.startBackend(); + setConnectionMode('local'); + } + }) .catch(console.error) - .finally(() => { - setConnectionMode('local'); - setAuthChecked(true); - }); + .finally(() => setAuthChecked(true)); } }, [isFirstLaunch, setupComplete, connectionMode]); // Initialize backend health monitoring for self-hosted mode useEffect(() => { - if (setupComplete && !isFirstLaunch && connectionMode === 'selfhosted') { + if (connectionMode !== 'selfhosted') { + // Stop the monitor whenever we leave selfhosted mode so the dot resets. + selfHostedServerMonitor.stop(); + return; + } + if (setupComplete && !isFirstLaunch) { void tauriBackendService.initializeExternalBackend(); - // Also start the self-hosted server monitor so the operation router and UI - // can detect when the remote server goes offline and fall back to local backend. connectionModeService.getServerConfig().then(cfg => { if (cfg?.url) { selfHostedServerMonitor.start(cfg.url); @@ -205,13 +207,16 @@ export function AppProviders({ children }: { children: ReactNode }) { return unsubscribe; }, [shouldPreloadLocalEndpoints, connectionMode]); - // Dispatch sign-in event only after authChecked=true so SignInModal is mounted. - // Using useEffect (not setTimeout) guarantees child effects (SignInModal's listener - // registration) run before this parent effect fires the event. + + // Dispatch sign-in modal after authChecked so SignInModal's listener is registered. + // (Child effects run before parent effects, so this fires after SignInModal mounts.) + // detail.locked is always false here: setPendingSignIn(true) is only called inside + // `if (!cfg?.lock_connection_mode)` branches above, so locked deployments never set + // pendingSignIn and therefore never reach this dispatch. useEffect(() => { if (!authChecked || !pendingSignIn) return; - window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT, { detail: pendingSignIn })); - setPendingSignIn(null); + window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT, { detail: { locked: false } })); + setPendingSignIn(false); }, [authChecked, pendingSignIn]); useEffect(() => { diff --git a/frontend/src/desktop/components/DesktopOnboardingModal.tsx b/frontend/src/desktop/components/DesktopOnboardingModal.tsx index 6fa13017d2..1526557ebe 100644 --- a/frontend/src/desktop/components/DesktopOnboardingModal.tsx +++ b/frontend/src/desktop/components/DesktopOnboardingModal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { Modal, Stack, Group, Button, ActionIcon } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import CloseIcon from '@mui/icons-material/Close'; @@ -24,20 +24,13 @@ export function DesktopOnboardingModal() { const { t } = useTranslation(); const [visible, setVisible] = useState(() => !localStorage.getItem(ONBOARDING_KEY)); const [step, setStep] = useState(0); - // null = still checking, true = locked (suppress modal), false = not locked (show modal) - const [isLocked, setIsLocked] = useState(null); - - // Provisioned (locked) deployments skip the onboarding entirely — the non-dismissible - // SignInModal handles authentication and shows the correct self-hosted login flow. - useEffect(() => { - connectionModeService.getCurrentConfig().then((cfg) => { - setIsLocked(cfg.lock_connection_mode && !!cfg.server_config?.url); - }); - }, []); const dismissFinal = () => { localStorage.setItem(ONBOARDING_KEY, 'true'); setVisible(false); + // If the user dismissed the sign-in slide without authenticating, fall back to local mode + // so the app is usable without a server connection. + connectionModeService.switchToLocal().catch(console.error); }; // X on slide 0 advances to sign-in slide rather than dismissing entirely @@ -63,7 +56,7 @@ export function DesktopOnboardingModal() { const welcomeSlide = useMemo(() => WelcomeSlide(), []); const totalSteps = 2; - if (!visible || isLocked === null || isLocked) return null; + if (!visible) return null; return ( void; } + export const SetupWizard: React.FC = ({ onComplete, noLayout = false, onClose }) => { const { t } = useTranslation(); const [activeStep, setActiveStep] = useState(SetupStep.SaaSLogin); @@ -313,10 +315,7 @@ export const SetupWizard: React.FC = ({ onComplete, noLayout = const loadLockedConfig = useCallback(async () => { const currentConfig = await connectionModeService.getCurrentConfig(); if (!currentConfig.lock_connection_mode) return; - // server_config may be null when the user switched to local mode from a locked deployment. - // Fall back to the URL saved by switchToLocal() so the wizard still shows locked login. - const serverUrl = currentConfig.server_config?.url - || localStorage.getItem('stirling-provisioned-server-url'); + const serverUrl = currentConfig.server_config?.url; if (!serverUrl) return; setLockConnectionMode(true); @@ -433,6 +432,26 @@ export const SetupWizard: React.FC = ({ onComplete, noLayout = > {t('setup.selfhosted.unreachable.retry', 'Retry')} + {lockConnectionMode ? ( + + {t('setup.selfhosted.unreachable.changeServer', 'Connect to a different server')} + + ) : ( + + )} - - )} +

+ +
)} diff --git a/frontend/src/desktop/components/shared/DisabledButtonWithTooltip.css b/frontend/src/desktop/components/shared/DisabledButtonWithTooltip.css new file mode 100644 index 0000000000..4bea117789 --- /dev/null +++ b/frontend/src/desktop/components/shared/DisabledButtonWithTooltip.css @@ -0,0 +1,38 @@ +.locked-button { + width: 100%; + text-align: center; + cursor: not-allowed; + user-select: none; + border-radius: var(--mantine-radius-sm); + color: var(--mantine-color-dimmed); + font-size: var(--mantine-font-size-md); + background-color: var(--mantine-color-blue-light); + padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); +} + +.locked-button-tooltip { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: calc(100% + 8px); + color: white; + white-space: nowrap; + pointer-events: none; + z-index: var(--mantine-z-index-popover); + box-shadow: var(--mantine-shadow-lg); + border-radius: var(--mantine-radius-sm); + font-size: var(--mantine-font-size-xs); + background-color: var(--mantine-color-dark-7); + padding: 6px var(--mantine-spacing-xs); +} + +.locked-button-tooltip-arrow { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 5px solid var(--mantine-color-dark-7); + border-left-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; +} diff --git a/frontend/src/desktop/components/shared/DisabledButtonWithTooltip.tsx b/frontend/src/desktop/components/shared/DisabledButtonWithTooltip.tsx new file mode 100644 index 0000000000..2262c9d079 --- /dev/null +++ b/frontend/src/desktop/components/shared/DisabledButtonWithTooltip.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import '@app/components/shared/DisabledButtonWithTooltip.css'; + +interface DisabledButtonWithTooltipProps { + /** Tooltip text shown on hover */ + tooltip: string; + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; +} + +/** + * A visually disabled button that still responds to hover (showing a tooltip). + * Mantine's disabled prop prevents pointer events entirely, so this is a plain + * div styled to match a disabled button with a custom hover tooltip. + */ +export function DisabledButtonWithTooltip({ tooltip, children, className, style }: DisabledButtonWithTooltipProps) { + const [hovered, setHovered] = React.useState(false); + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > +
+ {children} +
+ {hovered && ( +
+ {tooltip} +
+
+ )} +
+ ); +} diff --git a/frontend/src/desktop/services/connectionModeService.ts b/frontend/src/desktop/services/connectionModeService.ts index 09fd093957..bcf487d933 100644 --- a/frontend/src/desktop/services/connectionModeService.ts +++ b/frontend/src/desktop/services/connectionModeService.ts @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api/core'; import { fetch } from '@tauri-apps/plugin-http'; import { endpointAvailabilityService } from '@app/services/endpointAvailabilityService'; +import { selfHostedServerMonitor } from '@app/services/selfHostedServerMonitor'; export type ConnectionMode = 'saas' | 'selfhosted' | 'local'; @@ -37,6 +38,7 @@ export interface ConnectionTestResult { } export const LOCAL_MODE_STORAGE_KEY = 'stirling-local-mode'; +export const JWT_EXPIRED_PROMPTED_KEY = 'stirling-jwt-expired-prompted'; export class ConnectionModeService { private static instance: ConnectionModeService; @@ -87,8 +89,11 @@ export class ConnectionModeService { const localFlag = localStorage.getItem(LOCAL_MODE_STORAGE_KEY); - if (config.mode === 'saas' && localFlag === 'true') { - // User previously chose local-only mode. + if (localFlag === 'true') { + // User previously chose local-only mode (signed out or explicitly went offline). + // Applies to both 'saas' and 'selfhosted' store modes — the Rust guard on locked + // deployments can't change 'selfhosted' to 'saas' in the store, so we check the + // flag regardless of what the store says. config.mode = 'local'; } else if ( config.mode === 'saas' && @@ -122,8 +127,9 @@ export class ConnectionModeService { throw new Error('Connection mode is locked by provisioning'); } - // Clear local-only flag if switching to a real account + // Clear local-only flag and expiry-prompted flag when signing in localStorage.removeItem(LOCAL_MODE_STORAGE_KEY); + localStorage.removeItem(JWT_EXPIRED_PROMPTED_KEY); console.log('Switching to SaaS mode'); @@ -153,18 +159,17 @@ export class ConnectionModeService { // the 'local' distinction purely on the TypeScript side. localStorage.setItem(LOCAL_MODE_STORAGE_KEY, 'true'); - // When a locked provisioned deployment falls back to local, preserve the server URL - // so the SetupWizard can pre-fill it if the user tries to sign in again. - if (this.currentConfig?.lock_connection_mode && this.currentConfig.server_config?.url) { - localStorage.setItem('stirling-provisioned-server-url', this.currentConfig.server_config.url); - } - await invoke('set_connection_mode', { mode: 'saas', serverConfig: null, }); - this.currentConfig = { mode: 'local', server_config: null, lock_connection_mode: this.currentConfig?.lock_connection_mode ?? false }; + // For locked deployments, preserve server_config so the sign-in form can still + // show the correct server URL if the user wants to sign in later. + const isLocked = this.currentConfig?.lock_connection_mode ?? false; + const preservedServerConfig = isLocked ? (this.currentConfig?.server_config ?? null) : null; + + this.currentConfig = { mode: 'local', server_config: preservedServerConfig, lock_connection_mode: isLocked }; // Clear endpoint availability cache when mode changes endpointAvailabilityService.clearCache(); @@ -173,8 +178,9 @@ export class ConnectionModeService { } async switchToSelfHosted(serverConfig: ServerConfig): Promise { - // Clear local-only flag if switching to a real account + // Clear local-only flag and expiry-prompted flag when signing in localStorage.removeItem(LOCAL_MODE_STORAGE_KEY); + localStorage.removeItem(JWT_EXPIRED_PROMPTED_KEY); console.log('Switching to self-hosted mode:', serverConfig); @@ -189,6 +195,10 @@ export class ConnectionModeService { endpointAvailabilityService.clearCache(); console.log('Cleared endpoint availability cache due to connection mode change'); + // Single authoritative calling point for health monitoring — every path that + // switches to self-hosted mode funnels through here. + selfHostedServerMonitor.start(serverConfig.url); + this.notifyListeners(); console.log('Switched to self-hosted mode successfully'); diff --git a/frontend/src/proprietary/services/licenseService.ts b/frontend/src/proprietary/services/licenseService.ts index 9767d5820f..4a4da273f2 100644 --- a/frontend/src/proprietary/services/licenseService.ts +++ b/frontend/src/proprietary/services/licenseService.ts @@ -441,7 +441,7 @@ const licenseService = { */ async getLicenseInfo(): Promise { try { - const response = await apiClient.get('/api/v1/admin/license-info'); + const response = await apiClient.get('/api/v1/admin/license-info', { suppressErrorToast: true }); return response.data; } catch (error) { console.error('Error fetching license info:', error); From 8e200658a007454970c125db5529e188ef2b56b9 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:18:48 +0100 Subject: [PATCH 08/59] Alpha flag for file storage settings (#6044) ## Summary - Added "Alpha" badge to the File Storage & Sharing nav item in the settings sidebar - Added "Alpha" badge to the File Storage & Sharing page title - Removed the old inline "(Alpha)" text from the Enable Group Signing label - Restructured all toggle cards so the switch is anchored to the right of each row - Tightened spacing between cards for a more compact layout - Extended `ConfigNavItem` interface with optional `badge` and `badgeColor` fields for reuse elsewhere image --- .../core/components/shared/AppConfigModal.tsx | 7 +- .../shared/config/configNavSections.tsx | 2 + .../shared/config/configNavSections.tsx | 4 +- .../AdminStorageSharingSection.tsx | 184 ++++++++++-------- 4 files changed, 112 insertions(+), 85 deletions(-) diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx index 30253ea45d..694ec5c737 100644 --- a/frontend/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/src/core/components/shared/AppConfigModal.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react'; -import { Modal, Text, ActionIcon, Tooltip, Group } from '@mantine/core'; +import { Badge, Modal, Text, ActionIcon, Tooltip, Group } from '@mantine/core'; import { useNavigate, useLocation } from 'react-router-dom'; import LocalIcon from '@app/components/shared/LocalIcon'; import { useConfigNavSections } from '@app/components/shared/config/configNavSections'; @@ -186,6 +186,11 @@ const AppConfigModalInner: React.FC = ({ opened, onClose }) {item.label} + {item.badge && ( + + {item.badge} + + )} {showPlanWarning && ( , disabled: requiresLogin, - disabledTooltip: requiresLogin ? enableLoginTooltip : undefined + disabledTooltip: requiresLogin ? enableLoginTooltip : undefined, + badge: t('toolPanel.alpha', 'Alpha'), + badgeColor: 'orange', }, { key: 'adminEndpoints', diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx index b7ee40e621..86b858b64c 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminStorageSharingSection.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Anchor, Group, Loader, Paper, Stack, Switch, Text } from '@mantine/core'; +import { Anchor, Badge, Group, Loader, Paper, Stack, Switch, Text } from '@mantine/core'; import { useNavigate } from 'react-router-dom'; import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; @@ -124,42 +124,50 @@ export default function AdminStorageSharingSection() { return (
- +
- {t('admin.settings.storage.title', 'File Storage & Sharing')} + + {t('admin.settings.storage.title', 'File Storage & Sharing')} + {t('toolPanel.alpha', 'Alpha')} + {t('admin.settings.storage.description', 'Control server storage and sharing options.')}
- - - - {t('admin.settings.storage.enabled.label', 'Enable Server File Storage')} - {isFieldPending('enabled') && } - - - {t('admin.settings.storage.enabled.description', 'Allow users to store files on the server.')} - + + +
+ + {t('admin.settings.storage.enabled.label', 'Enable Server File Storage')} + {isFieldPending('enabled') && } + + + {t('admin.settings.storage.enabled.description', 'Allow users to store files on the server.')} + +
setSettings({ ...settings, enabled: e.currentTarget.checked })} disabled={!loginEnabled} styles={getDisabledStyles()} + style={{ flexShrink: 0 }} /> -
+
- - - - {t('admin.settings.storage.sharing.enabled.label', 'Enable Sharing')} - {isFieldPending('sharing.enabled') && } - - - {t('admin.settings.storage.sharing.enabled.description', 'Allow users to share stored files.')} - + + +
+ + {t('admin.settings.storage.sharing.enabled.label', 'Enable Sharing')} + {isFieldPending('sharing.enabled') && } + + + {t('admin.settings.storage.sharing.enabled.description', 'Allow users to share stored files.')} + +
@@ -170,35 +178,38 @@ export default function AdminStorageSharingSection() { } disabled={!loginEnabled || !storageEnabled} styles={getDisabledStyles()} + style={{ flexShrink: 0 }} /> -
+
- - - - {t('admin.settings.storage.sharing.links.label', 'Enable Share Links')} - {isFieldPending('sharing.linkEnabled') && } - - - {t('admin.settings.storage.sharing.links.description', 'Allow sharing via signed-in links.')} - - {!frontendUrlConfigured && ( - - {t('admin.settings.storage.sharing.links.frontendUrlNote', 'Requires a Frontend URL. ')} - { - e.preventDefault(); - navigate('/settings/adminGeneral#frontendUrl'); - }} - c="orange" - td="underline" - > - {t('admin.settings.storage.sharing.links.frontendUrlLink', 'Configure in System Settings')} - + + +
+ + {t('admin.settings.storage.sharing.links.label', 'Enable Share Links')} + {isFieldPending('sharing.linkEnabled') && } + + + {t('admin.settings.storage.sharing.links.description', 'Allow sharing via signed-in links.')} - )} + {!frontendUrlConfigured && ( + + {t('admin.settings.storage.sharing.links.frontendUrlNote', 'Requires a Frontend URL. ')} + { + e.preventDefault(); + navigate('/settings/adminGeneral#frontendUrl'); + }} + c="orange" + td="underline" + > + {t('admin.settings.storage.sharing.links.frontendUrlLink', 'Configure in System Settings')} + + + )} +
@@ -209,35 +220,38 @@ export default function AdminStorageSharingSection() { } disabled={!loginEnabled || !sharingEnabled || !frontendUrlConfigured} styles={getDisabledStyles()} + style={{ flexShrink: 0 }} /> -
+
- - - - {t('admin.settings.storage.sharing.email.label', 'Enable Email Sharing')} - {isFieldPending('sharing.emailEnabled') && } - - - {t('admin.settings.storage.sharing.email.description', 'Allow sharing with email addresses.')} - - {!mailEnabled && ( - - {t('admin.settings.storage.sharing.email.mailNote', 'Requires mail configuration. ')} - { - e.preventDefault(); - navigate('/settings/adminConnections'); - }} - c="orange" - td="underline" - > - {t('admin.settings.storage.sharing.email.mailLink', 'Configure Mail Settings')} - + + +
+ + {t('admin.settings.storage.sharing.email.label', 'Enable Email Sharing')} + {isFieldPending('sharing.emailEnabled') && } + + + {t('admin.settings.storage.sharing.email.description', 'Allow sharing with email addresses.')} - )} + {!mailEnabled && ( + + {t('admin.settings.storage.sharing.email.mailNote', 'Requires mail configuration. ')} + { + e.preventDefault(); + navigate('/settings/adminConnections'); + }} + c="orange" + td="underline" + > + {t('admin.settings.storage.sharing.email.mailLink', 'Configure Mail Settings')} + + + )} +
@@ -248,19 +262,22 @@ export default function AdminStorageSharingSection() { } disabled={!loginEnabled || !sharingEnabled || !mailEnabled} styles={getDisabledStyles()} + style={{ flexShrink: 0 }} /> -
+
- - - - {t('admin.settings.storage.signing.enabled.label', 'Enable Group Signing (Alpha)')} - {isFieldPending('signing.enabled') && } - - - {t('admin.settings.storage.signing.enabled.description', 'Allow users to create multi-participant document signing sessions. Requires server file storage to be enabled.')} - + + +
+ + {t('admin.settings.storage.signing.enabled.label', 'Enable Group Signing')} + {isFieldPending('signing.enabled') && } + + + {t('admin.settings.storage.signing.enabled.description', 'Allow users to create multi-participant document signing sessions. Requires server file storage to be enabled.')} + +
@@ -271,8 +288,9 @@ export default function AdminStorageSharingSection() { } disabled={!loginEnabled || !storageEnabled} styles={getDisabledStyles()} + style={{ flexShrink: 0 }} /> -
+
Date: Wed, 1 Apr 2026 18:08:45 +0100 Subject: [PATCH 09/59] bump deps (#6041) bump deps and add a one week buffer to releases that we merge in to allow for vulnerabilities to be caught. --- frontend/package-lock.json | 2565 ++++++++++++++++++------------------ frontend/package.json | 2 +- 2 files changed, 1271 insertions(+), 1296 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f1ce75050c..24d5c95448 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -214,9 +214,9 @@ "license": "MIT" }, "node_modules/@atlaskit/pragmatic-drag-and-drop": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.7.tgz", - "integrity": "sha512-jX+68AoSTqO/fhCyJDTZ38Ey6/wyL2Iq+J/moanma0YyktpnoHxevjY1UNJHYp0NCburdQDZSL1ZFac1mO1osQ==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.9.tgz", + "integrity": "sha512-m/bcw5flyjfcF/rdX4JeomtIBrWuDNOwcQieiywHv7zkfIRmUC34Q9ZLeNGVoz73UiGsRqxysMuw4tC7lSJ89g==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.0.0", @@ -295,9 +295,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -310,9 +310,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -374,9 +374,9 @@ } }, "node_modules/@cantoo/pdf-lib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.1.tgz", - "integrity": "sha512-Nr/N5kR0xEzibtXei25E8LX9ThYsAN+Wob9jGZ1MSkMzWfxSo1fQwHc/BumE11bMMKEzn7jG5nT+kGlzAaAb2Q==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.5.tgz", + "integrity": "sha512-3eMHEaqKHt/G/q+6QjT06A3lz0S/a8x3+myiSN7FNeL3uWcedO0lpfs6TWofa4C03Z1wz3tWeHoa4CsI7DrTSA==", "license": "MIT", "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", @@ -484,9 +484,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", "dev": true, "funding": [ { @@ -498,7 +498,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -574,13 +582,13 @@ } }, "node_modules/@embedpdf/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.8.0.tgz", - "integrity": "sha512-ui0HR4fl7ndiGPw40kMBxXCO9gZHctV1u3Q+/XTd34ONYJ+Pa2LoWNVW2IuPDK7PgKzABPT2axntowlVLPP10g==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.9.1.tgz", + "integrity": "sha512-DlFV2o+tv9S+j4TeBVkRaIjjE9o3Tq3+hvJNoIOFtl87cR77UVQqEIRqOf61yk85Y+T2LfmnVPWjNcMuiKUh8w==", "license": "MIT", "dependencies": { - "@embedpdf/engines": "2.8.0", - "@embedpdf/models": "2.8.0" + "@embedpdf/engines": "2.9.1", + "@embedpdf/models": "2.9.1" }, "peerDependencies": { "preact": "^10.26.4", @@ -591,9 +599,9 @@ } }, "node_modules/@embedpdf/engines": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.8.0.tgz", - "integrity": "sha512-s749nppKxOcgvFraySKrwtiCt2VMXFe8TFuZUV5R7z8TtMagt6o5NOk6VsdvIpggUYxIsiKhLkFvAqvkNgcjng==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.9.1.tgz", + "integrity": "sha512-zyUdKgM2BZVzkqkbIMiTPAKIdH4M3bRV91P1nXUJUU94AmL9DrDrMkf2Nv6+S0bxGt19OpqDlclGSRZ8txhbGw==", "license": "MIT", "dependencies": { "@embedpdf/fonts-arabic": "1.0.0", @@ -603,8 +611,8 @@ "@embedpdf/fonts-latin": "1.0.0", "@embedpdf/fonts-sc": "1.0.0", "@embedpdf/fonts-tc": "1.0.0", - "@embedpdf/models": "2.8.0", - "@embedpdf/pdfium": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/pdfium": "2.9.1" }, "peerDependencies": { "preact": "^10.26.4", @@ -657,31 +665,31 @@ "license": "OFL-1.1" }, "node_modules/@embedpdf/models": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.8.0.tgz", - "integrity": "sha512-kk3Fm8exMmEX9Ce7VQePybmo04NQGdpsO3FsX1YOQqHpLVBk7tiTeOdetjBqI+YhQ2zWLa2naNKSOSGGzYLyxA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.9.1.tgz", + "integrity": "sha512-hUKj30D+a9dDOQlbqbrpjaECDPIcw/526Vo/s+eqJBY8zDNUkZ6meX+aVUrKg8+ApBm2dcEdVo3ff7KgNRhUGw==", "license": "MIT" }, "node_modules/@embedpdf/pdfium": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.8.0.tgz", - "integrity": "sha512-RlNLRNboF1Y6fNDy4sJ/a/FEYxATZyeM+n25r3KZJjG+RaM6bxBWXvWlFlGBU5Vx2eqQ5AzDAmIE9cn1agFmqA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.9.1.tgz", + "integrity": "sha512-GUu1rDF3XP8X7UpQNnOCvc/jAX/Tw0NoUpOF0aEksVZ6CoujBxtu84P3kWAxcJl6cicyIJq1GAp9szHBSYrTrQ==", "license": "MIT" }, "node_modules/@embedpdf/plugin-annotation": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.8.0.tgz", - "integrity": "sha512-h31dT0pvQjFSwsBLytL4BBLf3WDdz9kmAYNKR10filikge7MpgTzgVYFD0C6AOyy2qK1Y/vqworCyh+emVD5aA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.9.1.tgz", + "integrity": "sha512-aNtXjI3NUwz7kdmWsQIWzuS1QdZmuHXGCc+Kwl9u5O0PAgoj74OLsgoNEcFzz9m1rljyq3WPVnLczO6ByiifpQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0", - "@embedpdf/utils": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/utils": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-history": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", - "@embedpdf/plugin-selection": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-history": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", + "@embedpdf/plugin-selection": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -690,15 +698,15 @@ } }, "node_modules/@embedpdf/plugin-attachment": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.8.0.tgz", - "integrity": "sha512-g2jCwjhQsij9zz2JOxZJkIeLTAUxiBKsFh6K4hcsG45ougw/mI3WCw1f+bZlAPkZuOWPo2/nthsHb+wAlrcykQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.9.1.tgz", + "integrity": "sha512-7fPPHLWHZE9SXZRPnibo6o/AkuiknJQckdNgO16g9EdbPbO6IDNg/8e8pyjK4Knc+ckyyF4gnJ1lemWswSELUw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -707,15 +715,15 @@ } }, "node_modules/@embedpdf/plugin-bookmark": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.8.0.tgz", - "integrity": "sha512-ab49e17amEshweobU2GbtDEuRDHj89vRYiahkRq9nU1ACI16JIyh4t4fE/m/ucL9YhMMXldjwdtTGHceV566Jg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.9.1.tgz", + "integrity": "sha512-5qgz4yBFEi6h1cogvn9q9CBObGdNLVfkX809kVNPHuRIYsghEieqYkdoh1BNwdP/bm9+D7a6pcXZrVvbXs5e5w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -724,15 +732,15 @@ } }, "node_modules/@embedpdf/plugin-document-manager": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.8.0.tgz", - "integrity": "sha512-SS+IKJ2+rk4dHM3PvQQrvnfNdb5oOF1INR5r2w0MKo22C9WPD8Ncg1Jak/mCrGbSrJemW3pkIrKZhoREQtSrbA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.9.1.tgz", + "integrity": "sha512-TrqkZvPIQxcWikL9Fmm8/qAJvI+uG7ahPXuyGyyF0JxkrrHCkUMCL8SPJN/fntcPnLJRIi3/K/xR5Qnt/L0aAg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -741,15 +749,15 @@ } }, "node_modules/@embedpdf/plugin-export": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.8.0.tgz", - "integrity": "sha512-AvqfyhB58HLoZMKyXLLT+1ebE7wrMEnIAjr+OqEuaNnekxvb8atD8EBByzgtXLiNLStlxFn5g1CKyQThpw4Pcw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.9.1.tgz", + "integrity": "sha512-oYl8H0km1m75SYz17mVwxm9ljZvW0yBV8sTcyCa7YhnqE7yxtczu7jw3BrU3KW4Sua9mr9izhs3AixvWkTASQg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -758,15 +766,15 @@ } }, "node_modules/@embedpdf/plugin-history": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.8.0.tgz", - "integrity": "sha512-S6TO7DqMqVtBYsztgvPvq8BOJTMl8rWdGjVMuoxD93HZdSgogoculwCATrJGor/BC+X6Vmtaqg6NJWSdIAeBEQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.9.1.tgz", + "integrity": "sha512-3AcvSTT7fmqe1ve/FvR3lJ5q7t5JYmnnAg8LKc9ATsDjS9J5b0WE03Omz9a8/sL19iKq8xeR1+W28phgvlcKNw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -775,15 +783,15 @@ } }, "node_modules/@embedpdf/plugin-interaction-manager": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.8.0.tgz", - "integrity": "sha512-xdRTAp1YiXWm+3WVqIN8dkRT3I/dHTumLJy5Kvt7lc1W2XM3M5bfCk5eTTcjY1DPv2buyt44i/4XNWoXAgBXDg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.9.1.tgz", + "integrity": "sha512-/wpdStr1NeyMCvAEMVSCPC0a3zaMd+TSK4u8INsIo3b1RoFfb9iTlBB+qW/aaxvZJ/C7MChQ7cLX6VSKXK/6JQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -792,17 +800,17 @@ } }, "node_modules/@embedpdf/plugin-pan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.8.0.tgz", - "integrity": "sha512-EFqTEHk9E7AMRwguRO24jRl0J/5+pG07wlAm5U2rB0LDk30oAqVklFuDygaALdi8ZRBdkWOfD3Wl3gurQvfwAA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.9.1.tgz", + "integrity": "sha512-LH5fp/2xKWuEYb5cc5jNWdXkhgOL+8TEf4oLcgao6hK33aV1xry+HAg5ATy+OPpIJItCwjU/Lk7Mc7uibYcFlA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -811,15 +819,15 @@ } }, "node_modules/@embedpdf/plugin-print": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.8.0.tgz", - "integrity": "sha512-PUdw2/1GwbewYrxVgsb+3lkYyYYcLU5qpazrBZjjx0v+SEpb2SMQOOPNUILeJGKbYUlxSz3qv/loFwqzALkUQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.9.1.tgz", + "integrity": "sha512-brTn0R8AVyAfpm0SYxz/8bg0e373n4KaeIgw5E51DSrczVr3zIgqYGSvXFbfbv57ZUp63Hb93sVorP1et2AeUw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=18.0.0", "react-dom": ">=18.0.0", @@ -828,20 +836,20 @@ } }, "node_modules/@embedpdf/plugin-redaction": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.8.0.tgz", - "integrity": "sha512-pIDFNd9rX7cwrhY6rFCBa5MTnGdLZHX7magToqna/Ffs1KEr0CfNF8jIXG0/E6KB6SsppbHwC7AkwRVWz8NoHg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.9.1.tgz", + "integrity": "sha512-2R81U4ex/JU4IAJ0+G3eIMOwpq80HnAIZ2sI5yMhkDbz+ZZE/sdAP4JKTOFIbhTkwk7DmC222yW34dQJoX4tRQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0", - "@embedpdf/utils": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/utils": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-annotation": "2.8.0", - "@embedpdf/plugin-history": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", - "@embedpdf/plugin-selection": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-annotation": "2.9.1", + "@embedpdf/plugin-history": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", + "@embedpdf/plugin-selection": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -850,15 +858,15 @@ } }, "node_modules/@embedpdf/plugin-render": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.8.0.tgz", - "integrity": "sha512-jVGSuyg366LmFzbDpqszLbu3G6VOfv1u46D1C0ph6pL3jisTlRswRosaXS4eVE/fAYTryBrI0olCpVYYct4bQw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.9.1.tgz", + "integrity": "sha512-mtfu6uDxlz3+j0xPXfKyvuu8iCFjapPkbnx8vGQ0z2PBNAMm+05hsNIzxJSGMP2VCFo09SOz2zCs7ch9J6NeNg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -867,15 +875,15 @@ } }, "node_modules/@embedpdf/plugin-rotate": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.8.0.tgz", - "integrity": "sha512-lWATWEwhkBW77dJaKXlSJbqEMLTSy1lOhn0kDAZ5eyPZRo+7UWe7LHuT5HWzzRlbY9rpvnyixCT6Zp9rKT+WnQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.9.1.tgz", + "integrity": "sha512-OgfMI2IsSPHKs4A0DGpmHpxuhoBcZjvw+tz+CXGBi6ILL6oS7z5wXhoMZfRgCgkvjII8RnmjBvmLeahoSaepAA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -884,16 +892,16 @@ } }, "node_modules/@embedpdf/plugin-scroll": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.8.0.tgz", - "integrity": "sha512-l6hFH6lsAI+07ZGuOwbC8qcRNdYzWSIfRszjt9UmhKZbvXLEp6YJeS/XOUC/37Kqi30tsmLDyZhMW1AooGAr1A==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.9.1.tgz", + "integrity": "sha512-+U3PSIUuNlIOTXzRhnPBP+Rx20sFOd3OPiowyI2EP/Kx/j5R/amgL/t2rjrpw9gjXEMEGsli9Fn4UqnVgMrPaQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -902,15 +910,15 @@ } }, "node_modules/@embedpdf/plugin-search": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.8.0.tgz", - "integrity": "sha512-HZ7munHdAF2pJ1cT02yc5I0Xcr4b4CQ8GkhD4qhTpZK0yga/tQiDY2Jjstygv7XiZwHIDKvqOuQsQmOXDsEemw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.9.1.tgz", + "integrity": "sha512-NefZgXPfj1MW8i5bYEfuphpXXAODutJTtwCHpLff1YwPw6liuBug9G1lDKSpNddvp+7aSkTcXV5LYNhcPxb1vg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -919,17 +927,17 @@ } }, "node_modules/@embedpdf/plugin-selection": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.8.0.tgz", - "integrity": "sha512-DcPyOp2WKoVYVpbZIP5t+JsEmCL9Y7bxe8PmiEBtlm1lustuUezdWyA2G4wsGCR4I/5uNlY85qBGCBhL195sSA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.9.1.tgz", + "integrity": "sha512-dVLjiLGnZDo0xO7lZulLGl3cJ/mO7BcA3PGO2uMdhqSWK4tAF/DrakvwXdD581VBwXD/C25EJhxiNa2L7mU4wg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0", - "@embedpdf/utils": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/utils": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -938,15 +946,15 @@ } }, "node_modules/@embedpdf/plugin-spread": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.8.0.tgz", - "integrity": "sha512-0Ld5HERaG8cKyWW7ktojG3FK6angkzYnBnw7Rnqf4cQuNdO7nKSTslyxkrLzF3vvmKrB//2ghm/pRVkgci4BOw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.9.1.tgz", + "integrity": "sha512-s9J4tvoucNac8pUAHVhv3PWDWMZJyK0ikaG78VdGwh4G2iMo5HW1LKDJkJ29l1TjAuxhIoKihpfQbLgUgi7JHA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -955,16 +963,16 @@ } }, "node_modules/@embedpdf/plugin-thumbnail": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.8.0.tgz", - "integrity": "sha512-Dq/Cqsn4GClRXNWerqlXNH8WDWK/TtHxwq6yr2kqgXPo1FyREto4I7GfdO1hygTu0Dl6HK2auEVHBVC8QT53fw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.9.1.tgz", + "integrity": "sha512-TAEUkVxvvB5kh2VVGf9RMuJFq++CQgWpKJ62ed2GhB9WvVKP5gBYj7G5ff90TLCrAV8iXZL9ENKcHgGKxrZrYw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-render": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-render": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -973,18 +981,18 @@ } }, "node_modules/@embedpdf/plugin-tiling": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.8.0.tgz", - "integrity": "sha512-gPe5mG6hyyruki1eSuQYD2KDbo0Z0TxzSk8TcHIdEYYF6NlI2OghuO8Vczz5WLU8clX8xzn8c/c+tQMHEXfbZA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.9.1.tgz", + "integrity": "sha512-UQ/gr/Rdzj7sMsgvtuClw6Jq25fMr+OEdW4tQmg2bV/MqpKptpOEUZ5Aiay4M03X3cgoxhS8UvobRMnjlpYljA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-render": "2.8.0", - "@embedpdf/plugin-scroll": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-render": "2.9.1", + "@embedpdf/plugin-scroll": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -993,15 +1001,15 @@ } }, "node_modules/@embedpdf/plugin-viewport": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.8.0.tgz", - "integrity": "sha512-E16hc4yPA54XQGHp0Dy3OYyE8ilBaJE7LJirVmha4kMkP7XBu6xHNOJrXtq4GsZmdLQkf+x8ie2DDbWS+tcwnw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.9.1.tgz", + "integrity": "sha512-bVhBuZHTppKV+OB5lBLqXQv+5oW1A7kAIc5UzsImBwl6NpwH+2PdVkelfrF37yEqnEF/mdxobriWSP0aOVl93w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -1010,17 +1018,17 @@ } }, "node_modules/@embedpdf/plugin-zoom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.8.0.tgz", - "integrity": "sha512-HepSQ7NFYhMsQsgfnC/G3b3LOYtNCWkCME0C0jBLrEYsgqsWozf97jIgzfOAeJsugyjQmojpgLUNt3xE9CtG6Q==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.9.1.tgz", + "integrity": "sha512-FVOYj+AKTc2aCPrZHOrgbiYNTfBmiwVpFCedmqeGLYOIYTIeGzwJlyIHLU0PmIC/nuHcu1ZFwh0tH23fWAfVCQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-scroll": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-scroll": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -1029,9 +1037,9 @@ } }, "node_modules/@embedpdf/utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.8.0.tgz", - "integrity": "sha512-mt3DiQ8pnPk95q0zv7dXfN+y5fzJT2WtXyST8ziYEgmfhz0l2HT/MHCAwLzB3Whlnd2BdHThLfJyG1UD6pC84g==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.9.1.tgz", + "integrity": "sha512-IKe/k5DruzpuyGJBIoLEL9AKJ9rEBDLgi1eSCWQPyYkuLWTJg4rBzyGh57eS4S4K4dzQrlJ3Z9zGAW1mgbY2Jw==", "license": "MIT", "peerDependencies": { "preact": "^10.26.4", @@ -1188,9 +1196,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -1205,9 +1213,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -1222,9 +1230,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -1239,9 +1247,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -1256,9 +1264,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -1273,9 +1281,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -1290,9 +1298,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -1307,9 +1315,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -1324,9 +1332,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -1341,9 +1349,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -1358,9 +1366,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -1375,9 +1383,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -1392,9 +1400,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -1409,9 +1417,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -1426,9 +1434,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -1443,9 +1451,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -1460,9 +1468,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -1477,9 +1485,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -1494,9 +1502,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -1511,9 +1519,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -1528,9 +1536,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -1545,9 +1553,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -1562,9 +1570,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -1579,9 +1587,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -1596,9 +1604,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -1613,9 +1621,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -1659,37 +1667,37 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", - "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.2", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^10.2.1" + "minimatch": "^10.2.4" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^1.1.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1721,9 +1729,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", - "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1731,13 +1739,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { @@ -1745,9 +1753,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -1763,32 +1771,32 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react": { - "version": "0.27.18", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.18.tgz", - "integrity": "sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==", + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.1.7", - "@floating-ui/utils": "^0.2.10", + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -1797,12 +1805,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", - "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.5" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -1810,9 +1818,9 @@ } }, "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==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -1868,9 +1876,9 @@ } }, "node_modules/@iconify-json/material-symbols": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.58.tgz", - "integrity": "sha512-yPDXwGFNZ4Fq6O8NGbMGP7N4lVk8uX+oMwF3rIb6WRv6lID1W+pd9GN/KiM20rxZR36FjrG6TI5+x2LKLHdOnA==", + "version": "1.2.63", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.63.tgz", + "integrity": "sha512-R4PS/l8K6j+dk2P2MoYFLJgfbZ4YDo6XjCOpX6b1tvX+BhiSpSOjc1b6cnb/mvWe+JWBKlt4pcvPNiAijFLPnA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1996,9 +2004,9 @@ "license": "MIT" }, "node_modules/@mantine/core": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.15.tgz", - "integrity": "sha512-wBn/GogB4x7a2Uj7Ztt3amRaApjED+9XqfE4wyCLh88R7KV55k9vnTdCx+irI/GLOOu9tXNUGm3a4t5sTajwkQ==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", + "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.27.16", @@ -2009,55 +2017,55 @@ "type-fest": "^4.41.0" }, "peerDependencies": { - "@mantine/hooks": "8.3.15", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/dates": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.15.tgz", - "integrity": "sha512-4WlGHCOAE4in88rQFNlPVl14e7WFWb+YBqxmx4rvAXLj9xLgUxYJO44fva1eIOwNPlTqwbx+GgsEr/HwlcmDMg==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.18.tgz", + "integrity": "sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw==", "license": "MIT", "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { - "@mantine/core": "8.3.15", - "@mantine/hooks": "8.3.15", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/dropzone": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.3.15.tgz", - "integrity": "sha512-12bx1msHULi4D2/VV2PHTBBSshjax/ogLZEIAewX4tK0vRN3OKtA0qR+lqKhywUW4KYv4Z9Dr6O1LoGKHntrUA==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.3.18.tgz", + "integrity": "sha512-GaYUUl/382R7hl1g6heTCZ5a6T5x6qYPg0oID6ik/J0j7e5+XMZyTH5ITpaqpsBQ09GKKsF5y3iNehpSby8Kew==", "license": "MIT", "dependencies": { "react-dropzone": "15.0.0" }, "peerDependencies": { - "@mantine/core": "8.3.15", - "@mantine/hooks": "8.3.15", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/hooks": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.15.tgz", - "integrity": "sha512-AUSnpUlzttHzJht3CJ1YWi16iy6NWRwtyWO5RLGHHsmiW05DyG0qOPKF8+R5dLHuOCnl3XOu4roI2Y1ku9U04Q==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", + "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" } }, "node_modules/@maxim_mazurok/gapi.client.discovery-v1": { - "version": "0.4.20200806", - "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.4.20200806.tgz", - "integrity": "sha512-Jeo/KZqK39DI6ExXHcJ4lqnn1O/wEqboQ6eQ8WnNpu5eJ7wUnX/C5KazOgs1aRhnIB/dVzDe8wm62nmtkMIoaw==", + "version": "0.5.20200806", + "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.5.20200806.tgz", + "integrity": "sha512-oVq9hnnI5VhAtsx55iJbPz8NRfJtWFpI1kINKeuygzCvsx90b1GQDeN3MDUvhADXiQ7+Izs316cqBnJjoDxCow==", "dev": true, "license": "MIT", "dependencies": { @@ -2066,9 +2074,9 @@ } }, "node_modules/@maxim_mazurok/gapi.client.drive-v3": { - "version": "0.1.20260220", - "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.1.20260220.tgz", - "integrity": "sha512-ySN46cAYsMw6IiZ7a3eKeUqyH++eL4sPIFlgwu33l0mJHLevK4Qd5VxJOgMS8nBp44xKssCCBLRuRq091rp1WA==", + "version": "0.2.20260311", + "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.2.20260311.tgz", + "integrity": "sha512-2SVn8bIFZB9pq1JjqBNY/Agebv8gjHdr5k+ippSVsz07eWpl7d0Vxj4huX1O60ev0NDqrIx6a7w6MFFUIKlN6w==", "dev": true, "license": "MIT", "dependencies": { @@ -2086,9 +2094,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.8.tgz", - "integrity": "sha512-s9UHZo7QJVly7gNArEZkbbsimHqJZhElgBpXIJdehZ4OWXt+CCr0SBDgUCDJnQrqpd1dWK2dLq5rmO4mCBmI3w==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz", + "integrity": "sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==", "license": "MIT", "funding": { "type": "opencollective", @@ -2096,9 +2104,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.8.tgz", - "integrity": "sha512-88sWg/UJc1X82OMO+ISR4E3P58I3BjFVg0qkmDu7OWlN8VijneZD3ylFA+ImxuPjMHW3SHosfSJYy1fztoz0fw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.9.tgz", + "integrity": "sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6" @@ -2111,7 +2119,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^7.3.8", + "@mui/material": "^7.3.9", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -2122,16 +2130,16 @@ } }, "node_modules/@mui/material": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.8.tgz", - "integrity": "sha512-QKd1RhDXE1hf2sQDNayA9ic9jGkEgvZOf0tTkJxlBPG8ns8aS4rS8WwYURw2x5y3739p0HauUXX9WbH7UufFLw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", + "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/core-downloads-tracker": "^7.3.8", - "@mui/system": "^7.3.8", - "@mui/types": "^7.4.11", - "@mui/utils": "^7.3.8", + "@mui/core-downloads-tracker": "^7.3.9", + "@mui/system": "^7.3.9", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.9", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -2150,7 +2158,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.8", + "@mui/material-pigment-css": "^7.3.9", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -2171,13 +2179,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.8.tgz", - "integrity": "sha512-du5dlPZ9XL3xW2apHoGDXBI+QLtyVJGrXNCfcNYfP/ojkz1RQ0rRV6VG9Rkm1DqEFRG8mjjTL7zmE1Bvn1eR4A==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.9.tgz", + "integrity": "sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/utils": "^7.3.8", + "@mui/utils": "^7.3.9", "prop-types": "^15.8.1" }, "engines": { @@ -2198,9 +2206,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.8.tgz", - "integrity": "sha512-JHAeXQzS0tJ+Fq3C6J4TVDsW+yKhO4uuxuiLaopNStJeQYBIUCXpKYyUCcgXym4AmhbznQnv9RlHywSH6b0FOg==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.9.tgz", + "integrity": "sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -2232,16 +2240,16 @@ } }, "node_modules/@mui/system": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.8.tgz", - "integrity": "sha512-hoFRj4Zw2Km8DPWZp/nKG+ao5Jw5LSk2m/e4EGc6M3RRwXKEkMSG4TgtfVJg7dS2homRwtdXSMW+iRO0ZJ4+IA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz", + "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/private-theming": "^7.3.8", - "@mui/styled-engine": "^7.3.8", - "@mui/types": "^7.4.11", - "@mui/utils": "^7.3.8", + "@mui/private-theming": "^7.3.9", + "@mui/styled-engine": "^7.3.9", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.9", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1" @@ -2272,9 +2280,9 @@ } }, "node_modules/@mui/types": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.11.tgz", - "integrity": "sha512-fZ2xO9D08IKOxO2oUBi1nnVKH6oJUD+64cnv4YAaFoC0E5+i1+S5AHbNqqvZlYYsbPEQ6qEVwuBqY3jl5W4G+Q==", + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6" @@ -2289,13 +2297,13 @@ } }, "node_modules/@mui/utils": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.8.tgz", - "integrity": "sha512-kZRcE2620CBGr+XI8YMmwPj6WIPwSF7uMJjvSfqd8zXVvlz0MCJbzRRUGNf8NgflCLthdji2DdS643TeyJ3+nA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.9.tgz", + "integrity": "sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/types": "^7.4.11", + "@mui/types": "^7.4.12", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -2319,9 +2327,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.95.tgz", - "integrity": "sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", + "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==", "license": "MIT", "optional": true, "workspaces": [ @@ -2335,23 +2343,23 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.95", - "@napi-rs/canvas-darwin-arm64": "0.1.95", - "@napi-rs/canvas-darwin-x64": "0.1.95", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.95", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.95", - "@napi-rs/canvas-linux-arm64-musl": "0.1.95", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.95", - "@napi-rs/canvas-linux-x64-gnu": "0.1.95", - "@napi-rs/canvas-linux-x64-musl": "0.1.95", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.95", - "@napi-rs/canvas-win32-x64-msvc": "0.1.95" + "@napi-rs/canvas-android-arm64": "0.1.97", + "@napi-rs/canvas-darwin-arm64": "0.1.97", + "@napi-rs/canvas-darwin-x64": "0.1.97", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", + "@napi-rs/canvas-linux-arm64-musl": "0.1.97", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-musl": "0.1.97", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", + "@napi-rs/canvas-win32-x64-msvc": "0.1.97" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.95.tgz", - "integrity": "sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz", + "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==", "cpu": [ "arm64" ], @@ -2369,9 +2377,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.95.tgz", - "integrity": "sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz", + "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==", "cpu": [ "arm64" ], @@ -2389,9 +2397,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.95.tgz", - "integrity": "sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz", + "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==", "cpu": [ "x64" ], @@ -2409,9 +2417,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.95.tgz", - "integrity": "sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz", + "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==", "cpu": [ "arm" ], @@ -2429,9 +2437,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.95.tgz", - "integrity": "sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz", + "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==", "cpu": [ "arm64" ], @@ -2449,9 +2457,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.95.tgz", - "integrity": "sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz", + "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==", "cpu": [ "arm64" ], @@ -2469,9 +2477,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.95.tgz", - "integrity": "sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz", + "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==", "cpu": [ "riscv64" ], @@ -2489,9 +2497,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.95.tgz", - "integrity": "sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz", + "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==", "cpu": [ "x64" ], @@ -2509,9 +2517,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.95.tgz", - "integrity": "sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz", + "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==", "cpu": [ "x64" ], @@ -2529,9 +2537,9 @@ } }, "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.95.tgz", - "integrity": "sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz", + "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==", "cpu": [ "arm64" ], @@ -2549,9 +2557,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.95.tgz", - "integrity": "sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz", + "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==", "cpu": [ "x64" ], @@ -2677,12 +2685,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", - "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", + "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.5.1", + "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2693,9 +2701,9 @@ } }, "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", - "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", + "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2806,9 +2814,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", - "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -2870,9 +2878,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.1.tgz", - "integrity": "sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.1.tgz", + "integrity": "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" @@ -2895,9 +2903,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.354.0", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.354.0.tgz", - "integrity": "sha512-sfH1PiThX1YWkrZSls6zMuZcJWnvboCnZEJ3Z/OI8WgBmLDJfQpficbuLM3tgSLIchI22TPAkpwdT987iW6XIA==", + "version": "1.363.3", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.3.tgz", + "integrity": "sha512-Wslj6BrDwIEkqoahJFE0DbqgoGsB/F9BC3XtzBQdUzr04XhVNriGQ7/lves9eCFwrpSiOHv/5xfSShRwiP3ciA==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -3074,16 +3082,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -3095,9 +3103,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -3109,9 +3117,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -3123,9 +3131,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -3137,9 +3145,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -3151,9 +3159,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -3165,9 +3173,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], @@ -3179,9 +3187,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], @@ -3193,9 +3201,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], @@ -3207,9 +3215,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], @@ -3221,9 +3229,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", "cpu": [ "loong64" ], @@ -3235,9 +3243,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], @@ -3249,9 +3257,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", "cpu": [ "ppc64" ], @@ -3263,9 +3271,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", "cpu": [ "ppc64" ], @@ -3277,9 +3285,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], @@ -3291,9 +3299,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], @@ -3305,9 +3313,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], @@ -3319,9 +3327,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], @@ -3333,9 +3341,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], @@ -3347,9 +3355,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", "cpu": [ "x64" ], @@ -3361,9 +3369,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", "cpu": [ "arm64" ], @@ -3375,9 +3383,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -3389,9 +3397,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -3403,9 +3411,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", "cpu": [ "x64" ], @@ -3417,9 +3425,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -3493,9 +3501,9 @@ } }, "node_modules/@supabase/auth-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.97.0.tgz", - "integrity": "sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.100.0.tgz", + "integrity": "sha512-pdT3ye3UVRN1Cg0wom6BmyY+XTtp5DiJaYnPi6j8ht5i8Lq8kfqxJMJz9GI9YDKk3w1nhGOPnh6Qz5qpyYm+1w==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3505,9 +3513,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz", - "integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.100.0.tgz", + "integrity": "sha512-keLg79RPwP+uiwHuxFPTFgDRxPV46LM4j/swjyR2GKJgWniTVSsgiBHfbIBDcrQwehLepy09b/9QSHUywtKRWQ==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3516,10 +3524,16 @@ "node": ">=20.0.0" } }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, "node_modules/@supabase/postgrest-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz", - "integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.100.0.tgz", + "integrity": "sha512-xYNvNbBJaXOGcrZ44wxwp5830uo1okMHGS8h8dm3u4f0xcZ39yzbryUsubTJW41MG2gbL/6U57cA4Pi6YMZ9pA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3529,12 +3543,12 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz", - "integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.100.0.tgz", + "integrity": "sha512-2AZs00zzEF0HuCKY8grz5eCYlwEfVi5HONLZFoNR6aDfxQivl8zdQYNjyFoqN2MZiVhQHD7u6XV/xHwM8mCEHw==", "license": "MIT", "dependencies": { - "@types/phoenix": "^1.6.6", + "@supabase/phoenix": "^0.4.0", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" @@ -3544,9 +3558,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz", - "integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.100.0.tgz", + "integrity": "sha512-d4EeuK6RNIgYNA2MU9kj8lQrLm5AzZ+WwpWjGkii6SADQNIGTC/uiaTRu02XJ5AmFALQfo8fLl9xuCkO6Xw+iQ==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -3557,16 +3571,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz", - "integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.100.0.tgz", + "integrity": "sha512-r0tlcukejJXJ1m/2eG/Ya5eYs4W8AC7oZfShpG3+SIo/eIU9uIt76ZeYI1SoUwUmcmzlAbgch+HDZDR/toVQPQ==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.97.0", - "@supabase/functions-js": "2.97.0", - "@supabase/postgrest-js": "2.97.0", - "@supabase/realtime-js": "2.97.0", - "@supabase/storage-js": "2.97.0" + "@supabase/auth-js": "2.100.0", + "@supabase/functions-js": "2.100.0", + "@supabase/postgrest-js": "2.100.0", + "@supabase/realtime-js": "2.100.0", + "@supabase/storage-js": "2.100.0" }, "engines": { "node": ">=20.0.0" @@ -3583,9 +3597,9 @@ } }, "node_modules/@swc/core": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.13.tgz", - "integrity": "sha512-0l1gl/72PErwUZuavcRpRAQN9uSst+Nk++niC5IX6lmMWpXoScYx3oq/narT64/sKv/eRiPTaAjBFGDEQiWJIw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.21.tgz", + "integrity": "sha512-fkk7NJcBscrR3/F8jiqlMptRHP650NxqDnspBMrRe5d8xOoCy9MLL5kOBLFXjFLfMo3KQQHhk+/jUULOMlR1uQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -3601,16 +3615,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.13", - "@swc/core-darwin-x64": "1.15.13", - "@swc/core-linux-arm-gnueabihf": "1.15.13", - "@swc/core-linux-arm64-gnu": "1.15.13", - "@swc/core-linux-arm64-musl": "1.15.13", - "@swc/core-linux-x64-gnu": "1.15.13", - "@swc/core-linux-x64-musl": "1.15.13", - "@swc/core-win32-arm64-msvc": "1.15.13", - "@swc/core-win32-ia32-msvc": "1.15.13", - "@swc/core-win32-x64-msvc": "1.15.13" + "@swc/core-darwin-arm64": "1.15.21", + "@swc/core-darwin-x64": "1.15.21", + "@swc/core-linux-arm-gnueabihf": "1.15.21", + "@swc/core-linux-arm64-gnu": "1.15.21", + "@swc/core-linux-arm64-musl": "1.15.21", + "@swc/core-linux-ppc64-gnu": "1.15.21", + "@swc/core-linux-s390x-gnu": "1.15.21", + "@swc/core-linux-x64-gnu": "1.15.21", + "@swc/core-linux-x64-musl": "1.15.21", + "@swc/core-win32-arm64-msvc": "1.15.21", + "@swc/core-win32-ia32-msvc": "1.15.21", + "@swc/core-win32-x64-msvc": "1.15.21" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3622,9 +3638,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.13.tgz", - "integrity": "sha512-ztXusRuC5NV2w+a6pDhX13CGioMLq8CjX5P4XgVJ21ocqz9t19288Do0y8LklplDtwcEhYGTNdMbkmUT7+lDTg==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.21.tgz", + "integrity": "sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==", "cpu": [ "arm64" ], @@ -3639,9 +3655,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.13.tgz", - "integrity": "sha512-cVifxQUKhaE7qcO/y9Mq6PEhoyvN9tSLzCnnFZ4EIabFHBuLtDDO6a+vLveOy98hAs5Qu1+bb5Nv0oa1Pihe3Q==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.21.tgz", + "integrity": "sha512-//fOVntgowz9+V90lVsNCtyyrtbHp3jWH6Rch7MXHXbcvbLmbCTmssl5DeedUWLLGiAAW1wksBdqdGYOTjaNLw==", "cpu": [ "x64" ], @@ -3656,9 +3672,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.13.tgz", - "integrity": "sha512-t+xxEzZ48enl/wGGy7SRYd7kImWQ/+wvVFD7g5JZo234g6/QnIgZ+YdfIyjHB+ZJI3F7a2IQHS7RNjxF29UkWw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.21.tgz", + "integrity": "sha512-meNI4Sh6h9h8DvIfEc0l5URabYMSuNvyisLmG6vnoYAS43s8ON3NJR8sDHvdP7NJTrLe0q/x2XCn6yL/BeHcZg==", "cpu": [ "arm" ], @@ -3673,9 +3689,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.13.tgz", - "integrity": "sha512-VndeGvKmTXFn6AGwjy0Kg8i7HccOCE7Jt/vmZwRxGtOfNZM1RLYRQ7MfDLo6T0h1Bq6eYzps3L5Ma4zBmjOnOg==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.21.tgz", + "integrity": "sha512-QrXlNQnHeXqU2EzLlnsPoWEh8/GtNJLvfMiPsDhk+ht6Xv8+vhvZ5YZ/BokNWSIZiWPKLAqR0M7T92YF5tmD3g==", "cpu": [ "arm64" ], @@ -3690,9 +3706,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.13.tgz", - "integrity": "sha512-SmZ9m+XqCB35NddHCctvHFLqPZDAs5j8IgD36GoutufDJmeq2VNfgk5rQoqNqKmAK3Y7iFdEmI76QoHIWiCLyw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.21.tgz", + "integrity": "sha512-8/yGCMO333ultDaMQivE5CjO6oXDPeeg1IV4sphojPkb0Pv0i6zvcRIkgp60xDB+UxLr6VgHgt+BBgqS959E9g==", "cpu": [ "arm64" ], @@ -3706,10 +3722,44 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.21.tgz", + "integrity": "sha512-ucW0HzPx0s1dgRvcvuLSPSA/2Kk/VYTv9st8qe1Kc22Gu0Q0rH9+6TcBTmMuNIp0Xs4BPr1uBttmbO1wEGI49Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.21.tgz", + "integrity": "sha512-ulTnOGc5I7YRObE/9NreAhQg94QkiR5qNhhcUZ1iFAYjzg/JGAi1ch+s/Ixe61pMIr8bfVrF0NOaB0f8wjaAfA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.13.tgz", - "integrity": "sha512-5rij+vB9a29aNkHq72EXI2ihDZPszJb4zlApJY4aCC/q6utgqFA6CkrfTfIb+O8hxtG3zP5KERETz8mfFK6A0A==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.21.tgz", + "integrity": "sha512-D0RokxtM+cPvSqJIKR6uja4hbD+scI9ezo95mBhfSyLUs9wnPPl26sLp1ZPR/EXRdYm3F3S6RUtVi+8QXhT24Q==", "cpu": [ "x64" ], @@ -3724,9 +3774,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.13.tgz", - "integrity": "sha512-OlSlaOK9JplQ5qn07WiBLibkOw7iml2++ojEXhhR3rbWrNEKCD7sd8+6wSavsInyFdw4PhLA+Hy6YyDBIE23Yw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.21.tgz", + "integrity": "sha512-nER8u7VeRfmU6fMDzl1NQAbbB/G7O2avmvCOwIul1uGkZ2/acbPH+DCL9h5+0yd/coNcxMBTL6NGepIew+7C2w==", "cpu": [ "x64" ], @@ -3741,9 +3791,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.13.tgz", - "integrity": "sha512-zwQii5YVdsfG8Ti9gIKgBKZg8qMkRZxl+OlYWUT5D93Jl4NuNBRausP20tfEkQdAPSRrMCSUZBM6FhW7izAZRg==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.21.tgz", + "integrity": "sha512-+/AgNBnjYugUA8C0Do4YzymgvnGbztv7j8HKSQLvR/DQgZPoXQ2B3PqB2mTtGh/X5DhlJWiqnunN35JUgWcAeQ==", "cpu": [ "arm64" ], @@ -3758,9 +3808,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.13.tgz", - "integrity": "sha512-hYXvyVVntqRlYoAIDwNzkS3tL2ijP3rxyWQMNKaxcCxxkCDto/w3meOK/OB6rbQSkNw0qTUcBfU9k+T0ptYdfQ==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.21.tgz", + "integrity": "sha512-IkSZj8PX/N4HcaFhMQtzmkV8YSnuNoJ0E6OvMwFiOfejPhiKXvl7CdDsn1f4/emYEIDO3fpgZW9DTaCRMDxaDA==", "cpu": [ "ia32" ], @@ -3775,9 +3825,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.13.tgz", - "integrity": "sha512-XTzKs7c/vYCcjmcwawnQvlHHNS1naJEAzcBckMI5OJlnrcgW8UtcX9NHFYvNjGtXuKv0/9KvqL4fuahdvlNGKw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.21.tgz", + "integrity": "sha512-zUyWso7OOENB6e1N1hNuNn8vbvLsTdKQ5WKLgt/JcBNfJhKy/6jmBmqI3GXk/MyvQKd5SLvP7A0F36p7TeDqvw==", "cpu": [ "x64" ], @@ -3809,47 +3859,47 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.31.1", + "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.1" + "tailwindcss": "4.2.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-x64": "4.2.1", - "@tailwindcss/oxide-freebsd-x64": "4.2.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -3863,9 +3913,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -3879,9 +3929,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ "x64" ], @@ -3895,9 +3945,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ "x64" ], @@ -3911,9 +3961,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ "arm" ], @@ -3927,9 +3977,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ "arm64" ], @@ -3943,9 +3993,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ "arm64" ], @@ -3959,9 +4009,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ "x64" ], @@ -3975,9 +4025,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ "x64" ], @@ -3991,9 +4041,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4020,9 +4070,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ "arm64" ], @@ -4036,9 +4086,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ "x64" ], @@ -4052,25 +4102,25 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", - "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", - "tailwindcss": "4.2.1" + "tailwindcss": "4.2.2" } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.19", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.19.tgz", - "integrity": "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==", + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.19" + "@tanstack/virtual-core": "3.13.23" }, "funding": { "type": "github", @@ -4082,9 +4132,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.19", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.19.tgz", - "integrity": "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==", + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", "license": "MIT", "funding": { "type": "github", @@ -4102,9 +4152,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz", - "integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -4118,23 +4168,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.10.0", - "@tauri-apps/cli-darwin-x64": "2.10.0", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", - "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", - "@tauri-apps/cli-linux-arm64-musl": "2.10.0", - "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", - "@tauri-apps/cli-linux-x64-gnu": "2.10.0", - "@tauri-apps/cli-linux-x64-musl": "2.10.0", - "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", - "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", - "@tauri-apps/cli-win32-x64-msvc": "2.10.0" + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz", - "integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", "cpu": [ "arm64" ], @@ -4149,9 +4199,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz", - "integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", "cpu": [ "x64" ], @@ -4166,9 +4216,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz", - "integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", "cpu": [ "arm" ], @@ -4183,9 +4233,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz", - "integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", "cpu": [ "arm64" ], @@ -4200,9 +4250,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz", - "integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", "cpu": [ "arm64" ], @@ -4217,9 +4267,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz", - "integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", "cpu": [ "riscv64" ], @@ -4234,9 +4284,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz", - "integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", "cpu": [ "x64" ], @@ -4251,9 +4301,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz", - "integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", "cpu": [ "x64" ], @@ -4268,9 +4318,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz", - "integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", "cpu": [ "arm64" ], @@ -4285,9 +4335,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz", - "integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", "cpu": [ "ia32" ], @@ -4302,9 +4352,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz", - "integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", "cpu": [ "x64" ], @@ -4931,9 +4981,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", - "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -4945,12 +4995,6 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/phoenix": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -5018,17 +5062,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -5041,22 +5085,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -5072,14 +5116,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -5094,14 +5138,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5112,9 +5156,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -5129,15 +5173,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -5154,10 +5198,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", - "dev": true, + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5168,16 +5211,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5196,16 +5239,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5220,13 +5263,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5257,20 +5300,20 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-react-swc": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.3.tgz", - "integrity": "sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.0.tgz", + "integrity": "sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2", + "@rolldown/pluginutils": "1.0.0-rc.7", "@swc/core": "^1.15.11" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/@vitest/coverage-v8": { @@ -5423,13 +5466,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", - "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.29", + "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -5454,29 +5497,29 @@ "license": "MIT" }, "node_modules/@vue/compiler-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", - "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", - "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.29", - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, @@ -5487,67 +5530,67 @@ "license": "MIT" }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", - "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/reactivity": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", - "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", "peer": true, "dependencies": { - "@vue/shared": "3.5.29" + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", - "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", "peer": true, "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", - "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", "peer": true, "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/runtime-core": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", - "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { - "vue": "3.5.29" + "vue": "3.5.30" } }, "node_modules/@vue/shared": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", - "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, "node_modules/abbrev": { @@ -5728,9 +5771,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", "dev": true, "license": "MIT", "dependencies": { @@ -5762,9 +5805,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.26", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.26.tgz", - "integrity": "sha512-c6Hxv5eR12gQmANICaAGM967LGOXZ4SVAuwkiDrqPqZ5oReOnj/ZBtj3dyfwAnEV5qbzspNzjMM8lZENDK8f5A==", + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", "funding": [ { "type": "opencollective", @@ -5798,9 +5841,9 @@ } }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -5874,12 +5917,11 @@ } }, "node_modules/bare-fs": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", - "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", + "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -5900,12 +5942,11 @@ } }, "node_modules/bare-os": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz", - "integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -5916,20 +5957,18 @@ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", - "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz", + "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "streamx": "^2.21.0", + "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { @@ -5946,12 +5985,11 @@ } }, "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -5978,9 +6016,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -6056,9 +6094,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -6240,9 +6278,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "funding": [ { "type": "opencollective", @@ -6577,9 +6615,9 @@ } }, "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6610,9 +6648,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -6639,14 +6677,14 @@ "license": "MIT" }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -7130,9 +7168,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT", "peer": true }, @@ -7247,9 +7285,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -7275,14 +7313,14 @@ } }, "node_modules/dependency-tree": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.3.0.tgz", - "integrity": "sha512-T893F3p48rblazo45S/5jkFEvU8mzZ8obtNSyP2S1QCA8e9PpVH+hIakHnQYdnhitwQ8wo9btYJpQxnjiGm0Qg==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.4.0.tgz", + "integrity": "sha512-r4wZ1pfv8eQrnoWbIGdrJTVmlb0dkXdwBjKsotKO4gmfqrOsAMG+0+cfA5EZ3NO8umc85twXOl1eO27E5pjTzw==", "dev": true, "license": "MIT", "dependencies": { "commander": "^12.1.0", - "filing-cabinet": "^5.1.0", + "filing-cabinet": "^5.2.0", "precinct": "^12.2.0", "typescript": "^5.9.3" }, @@ -7348,9 +7386,9 @@ } }, "node_modules/detective-cjs": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.0.1.tgz", - "integrity": "sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.1.0.tgz", + "integrity": "sha512-Qt3S4IddVNDb+71lm+jmt5NznIsgcKlibTnrw9Zr91rT9vRwKp+73+ImqLTNrQj4YuOxnzrC7GwIAVwF7136XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7470,16 +7508,16 @@ } }, "node_modules/devalue": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", "license": "MIT", "peer": true }, "node_modules/devtools-protocol": { - "version": "0.0.1566079", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", - "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", "dev": true, "license": "BSD-3-Clause" }, @@ -7512,9 +7550,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -7552,61 +7590,6 @@ "dpdm": "lib/bin/dpdm.js" } }, - "node_modules/dpdm/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/dpdm/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/dpdm/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/dpdm/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7629,9 +7612,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7652,9 +7635,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7758,9 +7741,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", - "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "license": "MIT", "workspaces": [ "docs", @@ -7768,9 +7751,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7781,32 +7764,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escalade": { @@ -7864,18 +7847,18 @@ } }, "node_modules/eslint": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", - "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.2", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -7884,9 +7867,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.1", + "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7897,7 +7880,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.1", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -7920,9 +7903,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7982,9 +7965,9 @@ "peer": true }, "node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8040,13 +8023,14 @@ } }, "node_modules/esrap": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", - "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" } }, "node_modules/esrecurse": { @@ -8225,17 +8209,17 @@ } }, "node_modules/filing-cabinet": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.1.0.tgz", - "integrity": "sha512-xA3nKuR0N762AtUloSEbq4T+tOqNf1rZ3vgPW8Sijurqz9rvArjTpZhfrV1OxSrhX6OUoDGAONXo6liKZTNXKQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.2.0.tgz", + "integrity": "sha512-eNrCJGdYQY0tV+ACNesQ7vb2aMxD76NM7THayMn0Z5XBt1Tonr4vbVN+FbhHfekKGQG9O5UaciDDR7+dw8P9ZA==", "dev": true, "license": "MIT", "dependencies": { "app-module-path": "^2.2.0", "commander": "^12.1.0", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.20.0", "module-definition": "^6.0.1", - "module-lookup-amd": "^9.1.0", + "module-lookup-amd": "^9.1.1", "resolve": "^1.22.11", "resolve-dependency-path": "^4.0.1", "sass-lookup": "^6.1.0", @@ -8323,9 +8307,9 @@ } }, "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==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -8405,9 +8389,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -8556,9 +8540,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8583,6 +8567,28 @@ "node": ">= 14" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8596,10 +8602,43 @@ "node": ">=10.13.0" } }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "license": "MIT", "engines": { "node": ">=18" @@ -8848,26 +8887,26 @@ } }, "node_modules/i18next": { - "version": "25.8.13", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.13.tgz", - "integrity": "sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA==", + "version": "25.10.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.5.tgz", + "integrity": "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==", "funding": [ { "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" + "url": "https://www.locize.com/i18next" }, { "type": "individual", "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" } ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4" + "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5" @@ -9010,12 +9049,12 @@ "license": "ISC" }, "node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/internmap": { @@ -9578,19 +9617,19 @@ } }, "node_modules/license-report": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/license-report/-/license-report-6.8.1.tgz", - "integrity": "sha512-TxUUJNLTa+1kfYMK7uk7fLFtPrKMahX10K+pAY4UnGeNIJi+xWRzt/HrxpVlHj+9LG9Y2mhLzKFjHgAkj04ujg==", + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/license-report/-/license-report-6.8.2.tgz", + "integrity": "sha512-eWzJujDhPm5bKTrolTBt8mvL6YW3c5SY1kpqnt7GmTLU01rOtzjqe3sevOQLF2dPY7dV+VnSZIWV774Cgqz/Eg==", "license": "MIT", "dependencies": { "@kessler/tableify": "^1.0.2", "debug": "^4.4.3", "eol": "^0.10.0", "find-up-simple": "^1.0.1", - "got": "^14.6.0", - "ini": "^5.0.0", + "got": "^14.6.6", + "ini": "^6.0.0", "rc": "^1.2.8", - "semver": "^7.7.3", + "semver": "^7.7.4", "tablemark": "^4.1.0", "text-table": "^0.2.0", "visit-values": "^2.0.0" @@ -9612,9 +9651,9 @@ } }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -9627,23 +9666,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -9661,9 +9700,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -9681,9 +9720,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -9701,9 +9740,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -9721,9 +9760,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -9741,9 +9780,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -9761,9 +9800,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -9781,9 +9820,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -9801,9 +9840,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -9821,9 +9860,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -9841,9 +9880,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -9957,9 +9996,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -10062,9 +10101,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -10177,16 +10216,16 @@ } }, "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "ufo": "^1.6.3" } }, "node_modules/module-definition": { @@ -10292,9 +10331,9 @@ "optional": true }, "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==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-source-walk": { @@ -10751,15 +10790,15 @@ } }, "node_modules/pdfjs-dist": { - "version": "5.4.624", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz", - "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==", + "version": "5.5.207", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz", + "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==", "license": "Apache-2.0", "engines": { - "node": ">=20.16.0 || >=22.3.0" + "node": ">=20.19.0 || >=22.13.0 || >=24" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.88", + "@napi-rs/canvas": "^0.1.95", "node-readable-to-web-readable-stream": "^0.4.2" } }, @@ -10809,9 +10848,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10886,9 +10925,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -11160,9 +11199,9 @@ } }, "node_modules/posthog-js": { - "version": "1.354.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.354.0.tgz", - "integrity": "sha512-qrpToz7mN1PmEfo+Ob4Z8euX4z2p17LA0EAtFeyod3IVnlwnu+Ybea/oxVsPiq5YAPo+p5z73FcjF2yEJ7oZnA==", + "version": "1.363.3", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.3.tgz", + "integrity": "sha512-j1+MTbHO17kKXJMGDnaiW1EMOiA4AprE8EML6QnbSds+XbqHR2CdHa8T+/zIriZSoXlkZH4R+A4gY29lb5hdlA==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -11170,10 +11209,10 @@ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", - "@posthog/core": "1.23.1", - "@posthog/types": "1.354.0", + "@posthog/core": "1.24.1", + "@posthog/types": "1.363.3", "core-js": "^3.38.1", - "dompurify": "^3.3.1", + "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", @@ -11181,9 +11220,9 @@ } }, "node_modules/preact": { - "version": "10.28.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", - "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "version": "10.29.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", + "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", "license": "MIT", "funding": { "type": "opencollective", @@ -11395,9 +11434,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -11416,9 +11455,9 @@ } }, "node_modules/puppeteer": { - "version": "24.37.5", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.5.tgz", - "integrity": "sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ==", + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", + "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -11426,9 +11465,9 @@ "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1566079", - "puppeteer-core": "24.37.5", - "typed-query-selector": "^2.12.0" + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.40.0", + "typed-query-selector": "^2.12.1" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" @@ -11438,17 +11477,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.37.5", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.5.tgz", - "integrity": "sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ==", + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", + "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", "dev": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", - "devtools-protocol": "0.0.1566079", - "typed-query-selector": "^2.12.0", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" }, @@ -11457,9 +11496,9 @@ } }, "node_modules/puppeteer/node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11576,12 +11615,12 @@ } }, "node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", "license": "MIT", "dependencies": { - "clsx": "^1.1.1", + "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { @@ -11589,15 +11628,6 @@ "react-dom": ">= 16.3.0" } }, - "node_modules/react-draggable/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/react-dropzone": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz", @@ -11662,9 +11692,9 @@ "license": "MIT" }, "node_modules/react-number-format": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", - "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", + "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==", "license": "MIT", "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -11742,13 +11772,13 @@ } }, "node_modules/react-rnd": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", - "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", + "version": "10.5.3", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.3.tgz", + "integrity": "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==", "license": "MIT", "dependencies": { - "re-resizable": "6.11.2", - "react-draggable": "4.4.6", + "re-resizable": "^6.11.2", + "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { @@ -11763,9 +11793,9 @@ "license": "0BSD" }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -11785,12 +11815,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", "license": "MIT", "dependencies": { - "react-router": "7.13.1" + "react-router": "7.13.2" }, "engines": { "node": ">=20.0.0" @@ -12004,15 +12034,15 @@ } }, "node_modules/recharts": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", - "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", "license": "MIT", "workspaces": [ "www" ], "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", @@ -12220,15 +12250,15 @@ "license": "ISC" }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12242,31 +12272,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, @@ -12289,14 +12319,14 @@ "license": "MIT" }, "node_modules/sass-lookup": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.0.tgz", - "integrity": "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.1.tgz", + "integrity": "sha512-12dvZdQYTeKZ1ypjuiijZYuMZ1m0F+4+BkRX5yJi2WA9W3DBUrcdCt7bVuKlagHl11n8eYtalWDle+m98Ol2DA==", "dev": true, "license": "MIT", "dependencies": { "commander": "^12.1.0", - "enhanced-resolve": "^5.18.0" + "enhanced-resolve": "^5.20.0" }, "bin": { "sass-lookup": "bin/cli.js" @@ -12461,9 +12491,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -12612,9 +12642,9 @@ } }, "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", "dev": true, "license": "MIT", "dependencies": { @@ -12702,12 +12732,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -12875,9 +12905,9 @@ } }, "node_modules/svelte": { - "version": "5.53.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz", - "integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==", + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz", + "integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==", "license": "MIT", "peer": true, "dependencies": { @@ -12890,7 +12920,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -13022,15 +13052,15 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "license": "MIT", "engines": { "node": ">=6" @@ -13041,9 +13071,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", "dev": true, "license": "MIT", "dependencies": { @@ -13056,13 +13086,14 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -13073,7 +13104,6 @@ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "streamx": "^2.12.5" } @@ -13093,61 +13123,6 @@ "node": ">=18" } }, - "node_modules/test-exclude/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/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -13185,9 +13160,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "dev": true, "license": "MIT", "engines": { @@ -13230,9 +13205,9 @@ } }, "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==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -13273,22 +13248,22 @@ } }, "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.23" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, @@ -13306,9 +13281,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13342,9 +13317,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -13483,9 +13458,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", "dev": true, "license": "MIT" }, @@ -13504,16 +13479,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", - "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -13838,9 +13813,9 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.2.0.tgz", - "integrity": "sha512-g2k9z8B/1Bx7D4wnFjPLx9dyYGrqWMLTpwTtPHhcU+ElNZP2O4+4OsyaficiDClus0dzVhdGvoGFYMJxoXZ12Q==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz", + "integrity": "sha512-ekryzCw0ouAOE8tw4RvVL/dfqguXzumsV3FBKoKso4MQ1MUUrUXtl5RI4KpJQUNGqFEsg9kxl4EvDl02YtA9VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13857,7 +13832,7 @@ "url": "https://github.com/sponsors/sapphi-red" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-tsconfig-paths": { @@ -13914,9 +13889,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -14000,9 +13975,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -14029,17 +14004,17 @@ } }, "node_modules/vue": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", - "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-sfc": "3.5.29", - "@vue/runtime-dom": "3.5.29", - "@vue/server-renderer": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" @@ -14287,9 +14262,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14335,9 +14310,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/frontend/package.json b/frontend/package.json index c53d89c9a4..b4c1f6263c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -124,7 +124,7 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:install": "playwright install", - "update:minor": "npm outdated || npm update && npm audit fix && npm test", + "update:minor": "npm outdated || npm update --before=$(date -v-7d +%Y-%m-%d) && (npm audit fix --before=$(date -v-7d +%Y-%m-%d) || true) && npm test", "update:major": "npx npm-check-updates -u && npm install", "update:interactive": "npx npm-check-updates -i", "update:minor-strict": "npx npm-check-updates -u --target minor && npm install" From 04e52ee06d1ab734de81ed53cbfe47bae2f77a13 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello <3691490+PeterDaveHello@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:35:38 +0800 Subject: [PATCH 10/59] Restore English search aliases in zh-TW tags (#6039) # Description of Changes Preserve the translated zh-TW tags while restoring the English aliases used by frontend tool search. This keeps common English technical queries such as permissions or access control discoverable in the zh-TW locale. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [x] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. ## GitHub Copilot Pull Reuqest summary > This pull request significantly expands the keyword tags for a wide range of PDF-related tools and actions in the Traditional Chinese (`zh-TW`) translation file. The main goal is to improve searchability and discoverability of features by including a comprehensive set of English and Chinese keywords, synonyms, and related phrases for each tool. > > The most important changes include: > > **Localization and Search Optimization:** > > * Expanded the `tags` fields for all tools and actions under the `[home.*]` sections in `frontend/public/locales/zh-TW/translation.toml` to include a broad set of English and Chinese keywords, synonyms, and common search phrases. This enhances feature discoverability for users searching in either language. [[1]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3885-R3925) [[2]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3934-R4039) [[3]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4054-R4209) [[4]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4218-R4218) > > **Consistency and Coverage:** > > * Ensured that each tool/action now has a rich set of tags that cover various ways users might refer to the feature, including technical terms, synonyms, and related concepts (e.g., "merge", "combine", "join" for PDF merging). [[1]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3885-R3925) [[2]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3934-R4039) [[3]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4054-R4209) [[4]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4218-R4218) > > **Internationalization Improvements:** > > * Added English keywords alongside Chinese ones to support bilingual search and better serve users who may search using English terms in a localized interface. [[1]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3885-R3925) [[2]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3934-R4039) [[3]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4054-R4209) [[4]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4218-R4218) > > These changes collectively make it easier for users to find the features they need, regardless of the language or terminology they use. --- .../public/locales/zh-TW/translation.toml | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/frontend/public/locales/zh-TW/translation.toml b/frontend/public/locales/zh-TW/translation.toml index 5ff949526c..aea4417185 100644 --- a/frontend/public/locales/zh-TW/translation.toml +++ b/frontend/public/locales/zh-TW/translation.toml @@ -3882,47 +3882,47 @@ sortBy = "排序方式:" [home.addAttachments] desc = "在 PDF 中新增或移除內嵌檔案(附件)" -tags = "內嵌,附加,包含" +tags = "內嵌,附加,包含,embed,attach,include,attachments,attach files,embed files,include files,add files,file attachment,associated files,supplementary files" title = "新增附件" [home.addImage] desc = "在 PDF 的指定位置新增圖片" -tags = "插入,內嵌,放置" +tags = "插入,內嵌,放置,insert,embed,place,add image,insert image,place image,embed image,add photo,add picture,add logo,graphics,insert picture,place photo,PNG,JPG,JPEG" title = "新增圖片" [home.addPageNumbers] desc = "在文件的設定位置新增頁碼" -tags = "編號,頁碼,計數" +tags = "編號,頁碼,計數,number,pagination,count,add page numbers,page numbering,page numbers,footer,header,number pages,sequential,pagination tool" title = "新增頁碼" [home.addPassword] desc = "用密碼加密您的 PDF 檔案。" -tags = "加密,密碼,鎖定,安全,保護,安全性,加密,保障,機密,私人,限制存取" +tags = "加密,密碼,鎖定,安全,保護,安全性,加密,保障,機密,私人,限制存取,encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access" title = "新增密碼" [home.addStamp] desc = "在指定位置加入文字或影像圖章" -tags = "圖章,標記,印章" +tags = "圖章,標記,印章,stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" title = "新增圖章至 PDF" [home.addText] desc = "在 PDF 的任意位置新增自訂文字" -tags = "文字,註解,標籤" +tags = "文字,註解,標籤,text,annotation,label,add text,insert text,place text,text box,add label,add caption,type on PDF,write on PDF,add words,add note,text overlay,typewriter" title = "新增文字" [home.adjustContrast] desc = "調整 PDF 的對比度、飽和度和亮度" -tags = "對比,亮度,飽和度" +tags = "對比,亮度,飽和度,contrast,brightness,saturation,adjust colors,color correction,enhance,lighten,darken,improve quality,color balance,hue,vibrance" title = "調整顏色/對比度" [home.annotate] desc = "在檢視器中突顯、手繪、加入註釋與形狀" -tags = "註解,螢光標記,繪圖" +tags = "註解,螢光標記,繪圖,annotate,highlight,draw,markup,comment,notes,review,redline,feedback,markup tools,sticky notes,shapes,arrows,text box,freehand" title = "註解" [home.automate] desc = "將多個 PDF 動作串接,建立多步驟工作流程。適合重複性工作。" -tags = "工作流程,序列,自動化" +tags = "工作流程,序列,自動化,workflow,sequence,automation,automate,batch,batch processing,pipeline,chain,multi-step,recurring,scheduled,automatic,process multiple,bulk operations" title = "自動化" [home.formFill] @@ -3931,112 +3931,112 @@ title = "填寫表單" [home.autoRename] desc = "依偵測到的標頭自動重新命名 PDF 檔案" -tags = "自動偵測,依標頭,整理,重新命名" +tags = "自動偵測,依標頭,整理,重新命名,auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" title = "自動重新命名 PDF 檔案" [home.autoSizeSplitPDF] desc = "根據大小、頁數或檔案數將單一 PDF 分割為多個檔案" -tags = "自動,分割,大小" +tags = "自動,分割,大小,auto,split,size" title = "根據大小/數量自動分割" [home.autoSplitPDF] desc = "自動分割掃描的 PDF,使用實體掃描頁面分割器 QR Code" -tags = "自動,分割,QR" +tags = "自動,分割,QR,auto,split,auto split,QR code,QR split,barcode,automatic split,divider page,separator page,scan divider,batch scanning" title = "自動分割頁面" [home.bookletImposition] desc = "建立適合列印與裝訂的小冊子頁序與多頁版面" -tags = "小冊子,列印,裝訂" +tags = "小冊子,列印,裝訂,booklet,print,binding,imposition,booklet printing,saddle stitch,fold,pamphlet,brochure,print booklet,duplex,two-sided,signature,book layout,page imposition,print layout" title = "小冊子拼版" [home.certSign] desc = "使用憑證/金鑰(PEM/P12)簽章 PDF" -tags = "驗證,PEM,P12,官方,加密,簽署,憑證,PKCS12,JKS,伺服器,手動,自動" +tags = "驗證,PEM,P12,官方,加密,簽署,憑證,PKCS12,JKS,伺服器,手動,自動,authenticate,official,encrypt,sign,certificate,server,manual,auto,digital certificate,certificate signature,PKI,cryptographic signature,trusted signature" title = "使用憑證簽章" [home.changeMetadata] desc = "從 PDF 檔案中變更/移除/新增中繼資料" -tags = "編輯,修改,更新" +tags = "編輯,修改,更新,edit,modify,update,metadata,properties,document properties,author,title,subject,keywords,creator,producer,info,document info,file properties" title = "變更中繼資料" [home.changePermissions] desc = "變更文件限制與權限" -tags = "權限,限制,權利,存取控制,允許,拒絕,列印,複製,編輯,修改權限,安全設定,使用者權利" +tags = "權限,限制,權利,存取控制,允許,拒絕,列印,複製,編輯,修改權限,安全設定,使用者權利,permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights" title = "變更權限" [home.compare] desc = "比較並顯示 2 個 PDF 檔案的差異" -tags = "差異" +tags = "差異,difference,compare,diff,compare PDFs,compare documents,find differences,show differences,changes,what changed,track changes,revisions,version compare,side by side,contrast,delta" title = "比較" [home.compress] desc = "壓縮 PDF 以減少其檔案大小。" -tags = "壓縮,減少,最佳化" +tags = "壓縮,減少,最佳化,shrink,reduce,optimize,compress,smaller,downsize,file size,reduce size,minimize,make smaller,decrease size,optimize size" title = "壓縮" [home.convert] desc = "在不同格式之間轉換檔案" -tags = "轉換,變更" +tags = "轉換,變更,transform,change,convert,PDF to Word,PDF to Excel,PDF to image,Word to PDF,Excel to PDF,PowerPoint to PDF,HTML to PDF,export,import,file conversion,format change,save as" title = "轉換" [home.crop] desc = "裁剪 PDF 以減少其大小(保持文字!)" -tags = "裁切,剪裁,調整大小" +tags = "裁切,剪裁,調整大小,trim,cut,resize,crop,crop PDF,trim PDF,trim margins,remove margins,cut edges,trim borders,remove white space,crop pages,trim pages,reduce margins,set margins" title = "裁剪 PDF" [home.devAirgapped] desc = "連結至隔離網路設定指南" -tags = "離線,隔離,無網路,斷線,安全,網路隔離,獨立運作" +tags = "離線,隔離,無網路,斷線,安全,網路隔離,獨立運作,air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone" title = "隔離網路設定" [home.devApi] desc = "連結至 API 文件" -tags = "API,開發,文件" +tags = "API,開發,文件,development,documentation,developer,REST,integration,endpoints,programmatic,automation,scripting" title = "API" [home.devFolderScanning] desc = "連結至自動化資料夾掃描指南" -tags = "自動化,資料夾,掃描" +tags = "自動化,資料夾,掃描,automation,folder,scanning,watch folder,hot folder,automatic processing,batch,monitor folder,auto process,folder monitoring" title = "自動化資料夾掃描" [home.devSsoGuide] desc = "連結至 SSO 指南" -tags = "SSO,單一登入,驗證,SAML,OAuth,OIDC,登入,企業,身分提供者,IdP" +tags = "SSO,單一登入,驗證,SAML,OAuth,OIDC,登入,企業,身分提供者,IdP,single sign-on,authentication,login,enterprise,identity provider" title = "SSO 指南" [home.editTableOfContents] desc = "在 PDF 檔案中新增或編輯書籤和目錄" -tags = "書籤,目錄,編輯" +tags = "書籤,目錄,編輯,bookmarks,contents,edit,table of contents,TOC,outline,navigation,chapters,sections,add bookmarks,edit bookmarks,PDF outline" title = "編輯目錄" [home.extractImages] desc = "從 PDF 中提取所有圖片並將它們儲存到壓縮檔中" -tags = "擷取,儲存,匯出" +tags = "擷取,儲存,匯出,pull,save,export,extract images,get images,save images,export images,extract photos,extract pictures,pull images,download images,rip images,extract graphics,save photos" title = "提取圖片" [home.extractPages] desc = "從 PDF 檔案中擷取特定頁面" -tags = "擷取,選取,複製" +tags = "擷取,選取,複製,pull,select,copy,extract,extract pages,get pages,pull out,save pages,export pages,copy pages,select pages,specific pages" title = "提取頁面" [home.flatten] desc = "從 PDF 中移除所有互動元素和表單" -tags = "簡化,移除,互動" +tags = "簡化,移除,互動,simplify,remove,interactive,flatten,flatten form,remove form fields,make static,finalize form,lock form,disable editing,convert to image,non-editable" title = "平坦化" [home.getPdfInfo] desc = "取得 PDF 的所有可能資訊" -tags = "資訊,中繼資料,詳細" +tags = "資訊,中繼資料,詳細,info,metadata,details,PDF info,document info,properties,file info,get info,show info,view properties,document properties,statistics,page count,file details,inspect" title = "取得 PDF 的所有資訊" [home.manageCertificates] desc = "匯入、匯出或刪除用於簽署 PDF 的數位憑證檔。" -tags = "憑證,匯入,匯出" +tags = "憑證,匯入,匯出,certificates,import,export,manage certificates,digital certificates,certificate management,PFX,P12,keystore,import certificate,export certificate,certificate store,PKI" title = "管理憑證" [home.merge] desc = "輕鬆將多個 PDF 合併為一個。" -tags = "合併,連接,整合" +tags = "合併,連接,整合,combine,join,unite,merge,merge PDFs,combine PDFs,join PDFs,concatenate,append,stitch,combine files,join files,merge documents" title = "合併" [home.mobile] @@ -4051,162 +4051,162 @@ workspace = "工作區" [home.multiTool] desc = "合併、旋轉、重新排列和移除頁面" -tags = "多個,工具" +tags = "多個,工具,multiple,tools,multi-tool,all-in-one,swiss army,page organizer,page editor,edit pages,manage pages,organize,reorganize" title = "PDF 複合工具" [home.ocr] desc = "清理掃描並從 PDF 中的影像中偵測文字並重新新增為文字。" -tags = "擷取,掃描" +tags = "擷取,掃描,extract,scan,OCR,optical character recognition,text recognition,scan to text,image to text,scanned document,searchable PDF,make searchable,extract text,recognize text,read scanned" title = "OCR / 清理掃描" [home.overlay-pdfs] desc = "將 PDF 覆蓋在另一個 PDF 上" -tags = "疊加,合併,圖層,重疊,疊加 PDF,圖層 PDF,合併 PDF,堆疊,疊加頁面,背景,前景,合成" +tags = "疊加,合併,圖層,重疊,疊加 PDF,圖層 PDF,合併 PDF,堆疊,疊加頁面,背景,前景,合成,overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite" title = "覆蓋 PDF" [home.pageLayout] desc = "將 PDF 檔案的多個頁面合併到單一頁面" -tags = "版面,排列,組合" +tags = "版面,排列,組合,layout,arrange,combine,N-up,2-up,4-up,multiple per page,pages per sheet,layout pages,tile,grid layout,multi-page layout,combine on page,handout" title = "多頁版面配置" [home.pdfOrganiser] desc = "以任何順序移除/重新排列頁面" -tags = "整理,重新排列,重新排序" +tags = "整理,重新排列,重新排序,organize,rearrange,reorder,organise,arrange pages,sort,move pages,delete pages,remove pages,page management,page organizer,page organiser,resequence" title = "整理" [home.pdfTextEditor] desc = "檢視與編輯 Stirling PDF 的 JSON 匯出,支援群組文字編輯與重新產生 PDF" -tags = "編輯文字,修改文字,變更文字,編輯內容,更新文字,改寫,校正,修訂,文字編輯器,內容編輯器" +tags = "編輯文字,修改文字,變更文字,編輯內容,更新文字,改寫,校正,修訂,文字編輯器,內容編輯器,edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor" title = "PDF 文字編輯器" [home.pdfToSinglePage] desc = "將所有 PDF 頁面合併為一個大的單一頁面" -tags = "合併,整合,單頁" +tags = "合併,整合,單頁,combine,merge,single,single page,one page,merge to single,combine all,stitch pages,concatenate vertical,long page,poster" title = "PDF 轉單一大頁面" [home.read] desc = "檢視並註解 PDFs。可反白文字、繪圖或插入評論以供審閱與協作。" -tags = "檢視,開啟,顯示" +tags = "檢視,開啟,顯示,view,open,display,read,viewer,PDF viewer,PDF reader,open PDF,view PDF,display PDF,preview,browse" title = "閱讀" [home.redact] desc = "依據選取的文字、繪製的形狀和選取的頁面塗黑 PDF" -tags = "遮蔽,塗黑,隱藏" +tags = "遮蔽,塗黑,隱藏,censor,blackout,hide,redact,redaction,black out,block out,remove sensitive,hide text,privacy,confidential,GDPR,PII,sensitive data,permanently remove,cover up,legal redaction" title = "手動塗黑" [home.removeAnnotations] desc = "從 PDF 中移除所有註釋/註解" -tags = "刪除,清理,去除" +tags = "刪除,清理,去除,delete,clean,strip,remove annotations,remove comments,delete comments,remove markup,remove highlights,clean annotations,strip comments,remove notes,delete markup,clear comments" title = "移除註釋" [home.removeBlanks] desc = "偵測並從文件中移除空白頁面" -tags = "刪除,清理,空白" +tags = "刪除,清理,空白,delete,clean,empty,remove blank,delete blank pages,empty pages,white pages,remove empty,clean up,cleanup blank" title = "移除空白頁面" [home.removeCertSign] desc = "從 PDF 移除簽章" -tags = "移除,刪除,解鎖" +tags = "移除,刪除,解鎖,remove,delete,unlock,remove certificate,remove signature,delete signature,unsigned,remove digital signature,strip signature,remove cert,unsign" title = "移除簽章" [home.removeImage] desc = "從 PDF 中移除圖片以減少檔案大小" -tags = "移除,刪除,清理" +tags = "移除,刪除,清理,remove,delete,clean,remove image,delete image,strip images,remove pictures,delete photos,clean images,reduce size,remove graphics" title = "移除圖片" [home.removePages] desc = "從您的 PDF 檔案中刪除不需要的頁面。" -tags = "刪除,擷取,排除" +tags = "刪除,擷取,排除,delete,extract,exclude,remove pages,delete pages,remove page,delete page,exclude pages,take out pages,discard pages,drop pages" title = "移除" [home.removePassword] desc = "從您的 PDF 檔案中移除密碼保護。" -tags = "解鎖" +tags = "解鎖,unlock,remove password,unlock PDF,decrypt,remove encryption,unprotect,open protected PDF,password removal,unlock protected,disable password,remove security,remove owner password" title = "移除密碼" [home.reorganizePages] desc = "透過視覺化拖放控制,重新排列、複製或刪除 PDF 頁面。" -tags = "重新排列,重新排序,整理" +tags = "重新排列,重新排序,整理,rearrange,reorder,organize,reorganize,move pages,page order,sort pages,arrange pages,shuffle,resequence" title = "重組頁面" [home.repair] desc = "嘗試修復損壞/破損的 PDF" -tags = "修復,還原" +tags = "修復,還原,fix,restore,repair,fix PDF,fix broken,fix corrupt,repair PDF,repair corrupt,broken PDF,corrupt PDF,damaged PDF,recover,fix errors,PDF won't open,can't open PDF,PDF errors,troubleshoot,restore PDF,rebuild,corrupted" title = "修復" [home.replaceColor] desc = "在 PDF 檔案中取代或反轉顏色" -tags = "取代顏色,反轉顏色,顏色取代,交換顏色,變更顏色,反轉,負片,顏色交換,尋找並取代顏色,轉換顏色,顏色變更" +tags = "取代顏色,反轉顏色,顏色取代,交換顏色,變更顏色,反轉,負片,顏色交換,尋找並取代顏色,轉換顏色,顏色變更,replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change" title = "取代與反轉顏色" [home.rotate] desc = "輕鬆旋轉您的 PDF。" -tags = "旋轉,翻轉,調整方向" +tags = "旋轉,翻轉,調整方向,turn,flip,orient,rotate,orientation,landscape,portrait,90 degrees,180 degrees,clockwise,anticlockwise,counter-clockwise,fix orientation" title = "旋轉" [home.sanitize] desc = "移除 PDF 中可能有害的元素" -tags = "清理,清除,移除" +tags = "清理,清除,移除,clean,purge,remove,sanitize,sanitise,remove scripts,remove javascript,remove metadata,strip metadata,security,clean document,remove hidden data,privacy" title = "淨化" [home.scalePages] desc = "修改頁面及其內容的大小/比例。" -tags = "調整大小,調整,縮放" +tags = "調整大小,調整,縮放,resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "調整頁面大小/比例" [home.scannerEffect] desc = "建立看起來像是掃描過的 PDF" -tags = "掃描,模擬,建立" +tags = "掃描,模擬,建立,scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" title = "掃描器效果" [home.scannerImageSplit] desc = "偵測並將掃描的照片分割為獨立頁面" -tags = "偵測,分割,照片" +tags = "偵測,分割,照片,detect,split,photos,auto detect,detect photos,split photos,separate photos,split scanned images,multiple photos,auto split,photo detection,image detection,scan separation" title = "偵測並分割掃描照片" [home.showJS] desc = "搜尋並顯示嵌入 PDF 中的任何 JS(JavaScript)" -tags = "JavaScript,程式碼,指令碼" +tags = "JavaScript,程式碼,指令碼,javascript,code,script,show javascript,show JS,find javascript,detect javascript,view javascript,embedded scripts,malware,security,inspect,debug" title = "顯示 JavaScript" [home.sign] desc = "透過繪圖、文字或影像新增簽章到 PDF" -tags = "簽名,署名" +tags = "簽名,署名,signature,autograph,e-sign,electronic signature,digital signature,sign document,approval,signoff,authorize,endorse,ink signature,handwriting" title = "簽章" [home.timestampPdf] desc = "新增 RFC 3161 文件時間戳記,以證明您的 PDF 於何時存在" -tags = "時間戳記,RFC 3161,TSA,時間戳記授權機構,文件時間戳記,存在證明,時間戳記權杖,可信時間戳記,簽署時間戳記,公證" +tags = "時間戳記,RFC 3161,TSA,時間戳記授權機構,文件時間戳記,存在證明,時間戳記權杖,可信時間戳記,簽署時間戳記,公證,timestamp,time stamp authority,document timestamp,proof of existence,timestamp token,trusted timestamp,sign timestamp,notarise" title = "PDF 加上時間戳記" [home.split] desc = "將 PDF 分割為多個檔案" -tags = "分割,分開,拆分" +tags = "分割,分開,拆分,divide,separate,break,split,extract pages,separate pages,divide document,break apart,separate files,unbind,split by page,divide by chapter" title = "分割" [home.splitByChapters] desc = "根據 PDF 的章節結構將其分割成多個檔案。" -tags = "分割,章節,結構" +tags = "分割,章節,結構,split,chapters,structure,split by chapters,split by bookmarks,bookmarks,outline,table of contents,TOC split,chapter split,divide by sections" title = "依章節分割 PDF" [home.splitBySections] desc = "將 PDF 的每頁分成較小的水平與垂直區塊" -tags = "分割,區塊,切分" +tags = "分割,區塊,切分,split,sections,divide,split by sections,grid split,divide pages,split into sections,cut pages,divide grid,section split,horizontal split,vertical split" title = "依區塊分割 PDF" [home.swagger] desc = "檢視 API 文件並測試端點" -tags = "API,文件,測試" +tags = "API,文件,測試,documentation,test,swagger,API docs,REST API,endpoints,developer,API reference,API testing,OpenAPI,integration,developer docs" title = "API 文件" [home.unlockPDFForms] desc = "移除 PDF 檔案中表單欄位的唯讀屬性" -tags = "解鎖,啟用,編輯" +tags = "解鎖,啟用,編輯,unlock,enable,edit,unlock forms,enable forms,editable forms,remove read only,make editable,unlock fields,enable editing,form fields,fillable,unprotect forms" title = "解鎖 PDF 表單" [home.validateSignature] desc = "驗證 PDF 檔案中的數位簽章與憑證" -tags = "驗證,校驗,憑證" +tags = "驗證,校驗,憑證,validate,verify,certificate,validate signature,verify signature,check signature,digital signature,certificate verification,signature validation,authentic,trust,signed,verify certificate" title = "驗證 PDF 簽章" [home.viewPdf] @@ -4215,7 +4215,7 @@ title = "檢視/編輯 PDF" [home.watermark] desc = "在您的 PDF 檔案中新增自訂浮水印。" -tags = "圖章,標記,覆蓋" +tags = "圖章,標記,覆蓋,stamp,mark,overlay,watermark,branding,logo,confidential,draft,copyright,trademark,text overlay,image overlay,background text" title = "新增浮水印" [HTMLToPDF] From 69eaa1c6b00c3e72a371b77e8c8dc2e65247a0f8 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:52:22 +0100 Subject: [PATCH 11/59] Pipeline changes and version bump (#6047) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a --- .../common/service/UserServiceInterface.java | 2 ++ .../api/pipeline/PipelineProcessor.java | 30 +++++++++++++++++-- .../api/pipeline/PipelineProcessorTest.java | 3 +- .../security/service/UserService.java | 9 ++++++ build.gradle | 4 +-- frontend/src-tauri/tauri.conf.json | 2 +- .../testing/serverExperienceSimulations.ts | 2 +- .../testing/serverExperienceSimulations.ts | 2 +- 8 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java b/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java index 074f422000..9649696567 100644 --- a/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java +++ b/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java @@ -5,6 +5,8 @@ public interface UserServiceInterface { String getCurrentUsername(); + String getCurrentUserApiKey(); + long getTotalUsersCount(); boolean isCurrentUserAdmin(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 1eac93b227..4d43faf629 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -37,7 +37,6 @@ import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; -import stirling.software.common.model.enumeration.Role; import stirling.software.common.service.UserServiceInterface; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @@ -84,9 +83,35 @@ public class PipelineProcessor { return name.substring(0, underscoreIndex) + extension; } + // Allowlist of URL path prefixes permitted through the pipeline. + private static final List ALLOWED_PIPELINE_PATH_PREFIXES = + List.of( + "/api/v1/general/", + "/api/v1/misc/", + "/api/v1/security/", + "/api/v1/convert/", + "/api/v1/filter/"); + + private void validatePipelineUrl(String url) { + // Strip scheme+host to get the path portion for comparison + String path = url; + int schemeEnd = url.indexOf("://"); + if (schemeEnd != -1) { + int pathStart = url.indexOf('/', schemeEnd + 3); + path = pathStart != -1 ? url.substring(pathStart) : "/"; + } + final String pathToCheck = path; + boolean allowed = ALLOWED_PIPELINE_PATH_PREFIXES.stream().anyMatch(pathToCheck::contains); + if (!allowed) { + log.warn("Blocked pipeline request to disallowed URL: {}", url); + throw new SecurityException( + "Pipeline operation not permitted for endpoint: " + pathToCheck); + } + } + private String getApiKeyForUser() { if (userService == null) return ""; - return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + return userService.getCurrentUserApiKey(); } private String getBaseUrl() { @@ -283,6 +308,7 @@ public class PipelineProcessor { /* package */ ResponseEntity sendWebRequest( String url, MultiValueMap body) { + validatePipelineUrl(url); RestTemplate restTemplate = new RestTemplate(); // Set up headers, including API key HttpHeaders headers = new HttpHeaders(); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java index d58770f45d..6b6ed82976 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java @@ -205,7 +205,8 @@ class PipelineProcessorTest { }); })) { ResponseEntity response = - pipelineProcessor.sendWebRequest("http://localhost/api", body); + pipelineProcessor.sendWebRequest( + "http://localhost/api/v1/general/merge-pdfs", body); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java index ea960207dd..e18c101e1e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java @@ -198,6 +198,15 @@ public class UserService implements UserServiceInterface { return user.getApiKey(); } + @Override + public String getCurrentUserApiKey() { + String username = getCurrentUsername(); + if (username == null || username.isEmpty()) { + throw new IllegalStateException("Cannot determine calling user for API key lookup"); + } + return getApiKeyForUser(username); + } + public boolean isValidApiKey(String apiKey) { return userRepository.findByApiKey(apiKey).isPresent(); } diff --git a/build.gradle b/build.gradle index b41c54e3b8..5acd760592 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ ext { springSecuritySamlVersion = "7.0.2" openSamlVersion = "5.2.1" commonmarkVersion = "0.27.1" - googleJavaFormatVersion = "1.35.0" + googleJavaFormatVersion = "1.28.0" logback = "1.5.32" junitPlatformVersion = "1.12.2" modernJavaVersion = 21 @@ -78,7 +78,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.8.0' + version = '2.9.0' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 44ef827c3f..84acfeba8a 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.8.0", + "version": "2.9.0", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/src/core/testing/serverExperienceSimulations.ts b/frontend/src/core/testing/serverExperienceSimulations.ts index bbd9b1702a..06bd8fdc76 100644 --- a/frontend/src/core/testing/serverExperienceSimulations.ts +++ b/frontend/src/core/testing/serverExperienceSimulations.ts @@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.8.0', + appVersion: '2.9.0', serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080, diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts index 4177f296ee..d7087533f4 100644 --- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.8.0', + appVersion: '2.9.0', serverCertificateEnabled: false, enableAlphaFunctionality: false, enableDesktopInstallSlide: true, From 1d844f6c806b4cb342cff4ff1d448bdcf8112a12 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:39:45 +0100 Subject: [PATCH 12/59] Fix/redact bug (#6048) --- frontend/src/core/components/AppLayout.tsx | 2 + .../core/components/pageEditor/PageEditor.tsx | 24 +++-- .../shared/NavigationWarningModal.tsx | 77 +++++++++------ .../tools/pdfTextEditor/PdfTextEditorView.tsx | 15 ++- .../tools/redact/ManualRedactionControls.tsx | 97 +++++++------------ .../tools/redact/RedactModeSelector.tsx | 20 ++-- .../core/components/viewer/EmbedPdfViewer.tsx | 47 +++++---- .../viewer/useViewerRightRailButtons.tsx | 6 +- .../src/core/contexts/NavigationContext.tsx | 31 +++++- frontend/src/core/tools/Redact.tsx | 3 +- 10 files changed, 180 insertions(+), 142 deletions(-) diff --git a/frontend/src/core/components/AppLayout.tsx b/frontend/src/core/components/AppLayout.tsx index 39de5dc650..9bcd31e6db 100644 --- a/frontend/src/core/components/AppLayout.tsx +++ b/frontend/src/core/components/AppLayout.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react'; import { useBanner } from '@app/contexts/BannerContext'; +import NavigationWarningModal from '@app/components/shared/NavigationWarningModal'; interface AppLayoutProps { children: ReactNode; @@ -26,6 +27,7 @@ export function AppLayout({ children }: AppLayoutProps) { {children}
+ ); } diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx index b182c9d3a8..1b965367d5 100644 --- a/frontend/src/core/components/pageEditor/PageEditor.tsx +++ b/frontend/src/core/components/pageEditor/PageEditor.tsx @@ -9,7 +9,6 @@ import '@app/components/pageEditor/PageEditor.module.css'; import PageThumbnail from '@app/components/pageEditor/PageThumbnail'; import DragDropGrid from '@app/components/pageEditor/DragDropGrid'; import SkeletonLoader from '@app/components/shared/SkeletonLoader'; -import NavigationWarningModal from '@app/components/shared/NavigationWarningModal'; import { FileId } from "@app/types/file"; import { GRID_CONSTANTS } from '@app/components/pageEditor/constants'; import { useInitialPageDocument } from '@app/components/pageEditor/hooks/useInitialPageDocument'; @@ -39,7 +38,7 @@ const PageEditor = ({ const { actions } = useFileActions(); // Navigation guard for unsaved changes - const { setHasUnsavedChanges } = useNavigationGuard(); + const { setHasUnsavedChanges, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers } = useNavigationGuard(); const navigationState = useNavigationState(); // Get PageEditor coordination functions @@ -393,6 +392,19 @@ const PageEditor = ({ updateCurrentPages, }); + // Register navigation warning handlers for the global modal + useEffect(() => { + registerNavigationWarningHandlers({ + onApplyAndContinue: async () => { + await applyChanges(); + }, + onExportAndContinue: async () => { + await onExportAll(); + }, + }); + return () => unregisterNavigationWarningHandlers(); + }, [applyChanges, onExportAll, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers]); + // Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument) const selectedPageCount = selectedPageIds.length; const activeFileIds = selectedFileIds; @@ -704,14 +716,6 @@ const PageEditor = ({ )} - { - await applyChanges(); - }} - onExportAndContinue={async () => { - await onExportAll(); - }} - />
); }; diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx index 6e143ccd18..8e80b5d771 100644 --- a/frontend/src/core/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx @@ -1,3 +1,4 @@ +import { useRef, useEffect } from "react"; import { Modal, Text, Button, Group, Stack } from "@mantine/core"; import { useNavigationGuard } from "@app/contexts/NavigationContext"; import { useTranslation } from "react-i18next"; @@ -6,51 +7,69 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import { Z_INDEX_TOAST } from "@app/styles/zIndex"; -interface NavigationWarningModalProps { - onApplyAndContinue?: () => Promise; - onExportAndContinue?: () => Promise; - /** Called when discarding - allows saving applied changes while discarding pending ones */ - onDiscardAndContinue?: () => Promise; -} - -const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDiscardAndContinue }: NavigationWarningModalProps) => { +const NavigationWarningModal = () => { const { t } = useTranslation(); - const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } = - useNavigationGuard(); + const { + showNavigationWarning, + hasUnsavedChanges, + pendingNavigation, + cancelNavigation, + setHasUnsavedChanges, + navigationWarningHandlersRef, + } = useNavigationGuard(); + + // Store pendingNavigation in a ref so async handlers always have the latest, + // not a stale closure captured before an await. + const pendingNavigationRef = useRef(pendingNavigation); + useEffect(() => { + pendingNavigationRef.current = pendingNavigation; + }, [pendingNavigation]); const handleKeepWorking = () => { cancelNavigation(); }; - const handleDiscardChanges = async () => { - // If a discard handler is provided, call it to save any already-applied changes, then discard the unsaved changes - if (onDiscardAndContinue) { - await onDiscardAndContinue(); - } + const finishAndNavigate = () => { + const nav = pendingNavigationRef.current; setHasUnsavedChanges(false); - confirmNavigation(); + cancelNavigation(); + if (nav) { + nav(); + } + }; + + const handleDiscardChanges = async () => { + const handlers = navigationWarningHandlersRef.current; + if (handlers?.onDiscardAndContinue) { + await handlers.onDiscardAndContinue(); + } + finishAndNavigate(); }; const handleApplyAndContinue = async () => { - if (onApplyAndContinue) { - await onApplyAndContinue(); + const handlers = navigationWarningHandlersRef.current; + if (handlers?.onApplyAndContinue) { + await handlers.onApplyAndContinue(); } - setHasUnsavedChanges(false); - confirmNavigation(); + finishAndNavigate(); }; const handleExportAndContinue = async () => { - if (onExportAndContinue) { - await onExportAndContinue(); + const handlers = navigationWarningHandlersRef.current; + if (handlers?.onExportAndContinue) { + await handlers.onExportAndContinue(); } - setHasUnsavedChanges(false); - confirmNavigation(); + finishAndNavigate(); }; + // Read handler availability at render time for button visibility + const handlers = navigationWarningHandlersRef.current; + const hasApply = !!handlers?.onApplyAndContinue; + const hasExport = !!handlers?.onExportAndContinue; + const BUTTON_WIDTH = "12rem"; // Only show modal if there are unsaved changes AND there's an actual pending navigation - // This prevents the modal from showing due to spurious state updates if (!hasUnsavedChanges || !pendingNavigation) { return null; } @@ -87,12 +106,12 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis - {onApplyAndContinue && ( + {hasApply && ( )} - {onExportAndContinue && ( + {hasExport && ( @@ -108,12 +127,12 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis - {onApplyAndContinue && ( + {hasApply && ( )} - {onExportAndContinue && ( + {hasExport && ( diff --git a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx index 99cd6ad99a..119ec4e381 100644 --- a/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx +++ b/frontend/src/core/components/tools/pdfTextEditor/PdfTextEditorView.tsx @@ -28,7 +28,7 @@ import CallSplitIcon from '@mui/icons-material/CallSplit'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import UploadFileIcon from '@mui/icons-material/UploadFileOutlined'; import { Rnd } from 'react-rnd'; -import NavigationWarningModal from '@app/components/shared/NavigationWarningModal'; +import { useNavigationGuard } from '@app/contexts/NavigationContext'; import { useFileContext } from '@app/contexts/FileContext'; import { @@ -415,6 +415,15 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => { } : null, }); + // Register navigation warning handlers for the global modal + const { registerNavigationWarningHandlers, unregisterNavigationWarningHandlers } = useNavigationGuard(); + useEffect(() => { + registerNavigationWarningHandlers({ + onApplyAndContinue: onSaveToWorkbench, + }); + return () => unregisterNavigationWarningHandlers(); + }, [onSaveToWorkbench, registerNavigationWarningHandlers, unregisterNavigationWarningHandlers]); + const clearSelection = useCallback(() => { setSelectedGroupIds(new Set()); lastSelectedGroupIdRef.current = null; @@ -2385,10 +2394,6 @@ const selectionToolbarPosition = useMemo(() => { )} - {/* Navigation Warning Modal */} - ); }; diff --git a/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx b/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx index 4c0fa41ef5..dd75d926d6 100644 --- a/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx +++ b/frontend/src/core/components/tools/redact/ManualRedactionControls.tsx @@ -1,10 +1,10 @@ import { useTranslation } from 'react-i18next'; import { useEffect, useRef, useCallback } from 'react'; import { Button, Stack, Text, Divider, ColorInput } from '@mantine/core'; -import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; import { useRedaction, useRedactionMode } from '@app/contexts/RedactionContext'; import { useViewer } from '@app/contexts/ViewerContext'; import { useSignature } from '@app/contexts/SignatureContext'; +import { useNavigationGuard } from '@app/contexts/NavigationContext'; interface ManualRedactionControlsProps { disabled?: boolean; @@ -27,40 +27,45 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda // Get signature context to deactivate annotation tools when switching to redaction const { signatureApiRef } = useSignature(); - // Check if redaction mode is active - const isRedactActive = isRedacting; - - // Track if we've auto-activated for the current bridge session - const hasAutoActivated = useRef(false); + // Check if user is navigating away (modal shown) — don't fight the save/leave process + const { showNavigationWarning } = useNavigationGuard(); // Track the previous file index to detect file switches const prevFileIndexRef = useRef(activeFileIndex); - // Auto-activate selection mode when the API bridge becomes ready - // This ensures Mark Text is pre-selected when entering manual redaction mode + // Guard: pause auto-reactivation during save/export to avoid interfering with EmbedPDF + const isSavingRef = useRef(false); + + // Keep redaction tool active at all times while this component is mounted. + // If anything deactivates it (annotation tools, text selection, file switch, etc.) + // this re-enables it automatically — no manual "Activate" button needed. useEffect(() => { - if (isBridgeReady && !disabled && !hasAutoActivated.current) { - hasAutoActivated.current = true; - // Small delay to ensure EmbedPDF is fully ready - const timer = setTimeout(() => { - // Deactivate annotation mode to show redaction layer + if (disabled || !isBridgeReady || isSavingRef.current || showNavigationWarning) return; + + if (!isRedacting || isAnnotationMode) { + // Kill annotation mode if it stole focus + if (isAnnotationMode) { setAnnotationMode(false); - // Pre-select the Redaction tool - activateManualRedact(); - }, 150); + if (signatureApiRef?.current) { + try { + signatureApiRef.current.deactivateTools(); + } catch (error) { + console.log('Unable to deactivate annotation tools:', error); + } + } + } + // Small delay to avoid racing with EmbedPDF's own state updates + const timer = setTimeout(() => { + if (!isSavingRef.current) { + activateManualRedact(); + } + }, 50); return () => clearTimeout(timer); } - }, [isBridgeReady, disabled, activateManualRedact, setAnnotationMode]); - - // Reset auto-activation flag when disabled changes or bridge becomes not ready - useEffect(() => { - if (disabled || !isBridgeReady) { - hasAutoActivated.current = false; - } - }, [disabled, isBridgeReady]); + }, [isRedacting, isAnnotationMode, disabled, isBridgeReady, showNavigationWarning, setAnnotationMode, signatureApiRef, activateManualRedact]); // Reset redaction tool when switching between files - // The new PDF gets a fresh EmbedPDF instance - forcing user to re-select tool ensures it works properly + // The new PDF gets a fresh EmbedPDF instance useEffect(() => { if (prevFileIndexRef.current !== activeFileIndex) { prevFileIndexRef.current = activeFileIndex; @@ -69,41 +74,24 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda if (activeType) { setActiveType(null); } - - // Reset auto-activation flag so new file can auto-activate - hasAutoActivated.current = false; } }, [activeFileIndex, activeType, setActiveType]); - const handleRedactClick = () => { - // Deactivate annotation mode and tools to switch to redaction layer - if (isAnnotationMode) { - setAnnotationMode(false); - // Deactivate any active annotation tools (like draw) - if (signatureApiRef?.current) { - try { - signatureApiRef.current.deactivateTools(); - } catch (error) { - console.log('Unable to deactivate annotation tools:', error); - } - } - } - - activateManualRedact(); - }; - // Handle saving changes - this will apply pending redactions and save to file const handleSaveChanges = useCallback(async () => { if (applyChanges) { - await applyChanges(); + isSavingRef.current = true; + try { + await applyChanges(); + } finally { + isSavingRef.current = false; + } } }, [applyChanges]); // Check if there are unsaved changes to save (pending redactions OR applied redactions) - // Save Changes button will apply pending redactions and then save everything const hasUnsavedChanges = pendingCount > 0 || redactionsApplied; - // Check if API is available - use isBridgeReady state instead of ref (refs don't trigger re-renders) const isApiReady = isBridgeReady; return ( @@ -128,18 +116,6 @@ export default function ManualRedactionControls({ disabled = false }: ManualReda popoverProps={{ withinPortal: true }} /> - - {/* Save Changes Button - applies pending redactions and saves to file */} + + ) : undefined, review: { isVisible: base.hasResults, operation: base.operation, diff --git a/frontend/src/core/tools/OCR.tsx b/frontend/src/core/tools/OCR.tsx index 07f05c812b..8d71517a0d 100644 --- a/frontend/src/core/tools/OCR.tsx +++ b/frontend/src/core/tools/OCR.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; -import { useFileSelection } from "@app/contexts/FileContext"; +import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles"; import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; @@ -16,7 +16,7 @@ import { useAdvancedOCRTips } from "@app/components/tooltips/useAdvancedOCRTips" const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); - const { selectedFiles } = useFileSelection(); + const selectedFiles = useViewScopedFiles(); const ocrParams = useOCRParameters(); const ocrOperation = useOCROperation(); diff --git a/frontend/src/core/tools/ReorganizePages.tsx b/frontend/src/core/tools/ReorganizePages.tsx index d70f7a2667..3d60a0d330 100644 --- a/frontend/src/core/tools/ReorganizePages.tsx +++ b/frontend/src/core/tools/ReorganizePages.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; import { useEndpointEnabled } from "@app/hooks/useEndpointConfig"; -import { useFileSelection } from "@app/contexts/FileContext"; +import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles"; import { useAccordionSteps } from "@app/hooks/tools/shared/useAccordionSteps"; import ReorganizePagesSettings from "@app/components/tools/reorganizePages/ReorganizePagesSettings"; import { useReorganizePagesParameters } from "@app/hooks/tools/reorganizePages/useReorganizePagesParameters"; @@ -11,7 +11,7 @@ import { useReorganizePagesOperation } from "@app/hooks/tools/reorganizePages/us const ReorganizePages = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); - const { selectedFiles } = useFileSelection(); + const selectedFiles = useViewScopedFiles(); const params = useReorganizePagesParameters(); const operation = useReorganizePagesOperation(); diff --git a/frontend/src/core/utils/textUtils.ts b/frontend/src/core/utils/textUtils.ts index e033d505a6..91f4db97e7 100644 --- a/frontend/src/core/utils/textUtils.ts +++ b/frontend/src/core/utils/textUtils.ts @@ -1,3 +1,16 @@ +/** + * Truncates text from the centre, preserving the start and end. + * e.g. "very-long-filename.pdf" -> "very-lo...ame.pdf" + */ +export function truncateCenter(text: string, maxLength: number = 25): string { + if (text.length <= maxLength) return text; + const ellipsis = '...'; + const charsToShow = maxLength - ellipsis.length; + const frontChars = Math.ceil(charsToShow / 2); + const backChars = Math.floor(charsToShow / 2); + return text.substring(0, frontChars) + ellipsis + text.substring(text.length - backChars); +} + /** * Filters out emoji characters from a text string * @param text - The input text string From 7d79ed4148220aa819e0f8ab145270b27de0a14a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:49:16 +0100 Subject: [PATCH 19/59] possible fix permission issues and fix thread timing issues (#6061) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/workflows/push-docker-base.yml | 3 + .github/workflows/rollback-latest.yml | 93 +++++++++++++++++++ .../configuration/SecurityConfiguration.java | 7 +- build.gradle | 2 +- docker/embedded/Dockerfile | 9 ++ docker/embedded/Dockerfile.fat | 9 ++ frontend/src-tauri/tauri.conf.json | 2 +- .../testing/serverExperienceSimulations.ts | 2 +- .../testing/serverExperienceSimulations.ts | 2 +- scripts/init-without-ocr.sh | 27 +++++- testing/test.sh | 38 +++++++- 11 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/rollback-latest.yml diff --git a/.github/workflows/push-docker-base.yml b/.github/workflows/push-docker-base.yml index 59561ed0df..699eb4708c 100644 --- a/.github/workflows/push-docker-base.yml +++ b/.github/workflows/push-docker-base.yml @@ -4,6 +4,7 @@ on: push: branches: - baseDockerImage + - accessIssueFix workflow_dispatch: inputs: version: @@ -34,6 +35,8 @@ jobs: run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then VERSION="${{ github.event.inputs.version }}" + elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then + VERSION="1.0.3" else VERSION="1.0.0" fi diff --git a/.github/workflows/rollback-latest.yml b/.github/workflows/rollback-latest.yml new file mode 100644 index 0000000000..21027cc762 --- /dev/null +++ b/.github/workflows/rollback-latest.yml @@ -0,0 +1,93 @@ +name: Rollback Latest Tags to Version + +on: + workflow_dispatch: + inputs: + version: + description: "Version to rollback to (e.g. 2.8.0)" + required: true + type: string + +permissions: + contents: read + +jobs: + rollback: + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Install crane + uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4 + + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_API }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT + + - name: Rollback all latest tags to v${{ inputs.version }} + env: + VERSION: ${{ inputs.version }} + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_ORG_USERNAME: ${{ secrets.DOCKER_HUB_ORG_USERNAME }} + REPO_OWNER: ${{ steps.repoowner.outputs.lowercase }} + run: | + set -euo pipefail + + IMAGES=( + "${DOCKER_HUB_USERNAME}/s-pdf" + "ghcr.io/${REPO_OWNER}/s-pdf" + "ghcr.io/${REPO_OWNER}/stirling-pdf" + "${DOCKER_HUB_ORG_USERNAME}/stirling-pdf" + ) + + VARIANTS=( + "${VERSION}:latest" + "${VERSION}-fat:latest-fat" + "${VERSION}-ultra-lite:latest-ultra-lite" + ) + + FAILED=0 + + for image in "${IMAGES[@]}"; do + for variant in "${VARIANTS[@]}"; do + SOURCE_TAG="${variant%%:*}" + TARGET_TAG="${variant##*:}" + + echo "::group::${image} — ${SOURCE_TAG} → ${TARGET_TAG}" + + if crane manifest "${image}:${SOURCE_TAG}" > /dev/null 2>&1; then + crane cp "${image}:${SOURCE_TAG}" "${image}:${TARGET_TAG}" + echo "✅ ${image}:${TARGET_TAG} now points to ${SOURCE_TAG}" + else + echo "::warning::⚠️ ${image}:${SOURCE_TAG} not found, skipping" + FAILED=1 + fi + + echo "::endgroup::" + done + done + + if [ "$FAILED" -ne 0 ]; then + echo "::warning::Some source tags were not found. This is expected if not all variants exist for this version." + fi + + echo "" + echo "🎉 Rollback to ${VERSION} complete!" diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 69c9d261b2..456c5a7c34 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -293,7 +293,12 @@ public class SecurityConfiguration { http.addFilterBefore( userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(rateLimitingFilter, UsernamePasswordAuthenticationFilter.class) + // TODO: IPRateLimitingFilter disabled — limit is 1M (no-op) and raw Filter + // impl causes Spring Security async dispatch bug (response already committed + // errors on StreamingResponseBody endpoints). Re-enable once converted to + // OncePerRequestFilter with proper config-driven limits. + // .addFilterBefore(rateLimitingFilter, + // UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class); http.sessionManagement( diff --git a/build.gradle b/build.gradle index 4a319a315f..5d50620e57 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.9.0' + version = '2.9.1' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index d44e616f59..0d8bb176e8 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -91,6 +91,15 @@ ENV VERSION_TAG=$VERSION_TAG \ _JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ _JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + DBUS_SESSION_BUS_ADDRESS=/dev/null \ SAL_TMP=/tmp/stirling-pdf/libre # Metadata labels diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index 6ad392d341..43d4da75a9 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -91,8 +91,17 @@ ENV VERSION_TAG=$VERSION_TAG \ _JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ _JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ FAT_DOCKER=true \ INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + DBUS_SESSION_BUS_ADDRESS=/dev/null \ SAL_TMP=/tmp/stirling-pdf/libre # Metadata labels diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 84acfeba8a..aa6580bdb5 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.9.0", + "version": "2.9.1", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/src/core/testing/serverExperienceSimulations.ts b/frontend/src/core/testing/serverExperienceSimulations.ts index 06bd8fdc76..4d40d7b7e8 100644 --- a/frontend/src/core/testing/serverExperienceSimulations.ts +++ b/frontend/src/core/testing/serverExperienceSimulations.ts @@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.9.0', + appVersion: '2.9.1', serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080, diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts index d7087533f4..69d4205506 100644 --- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.9.0', + appVersion: '2.9.1', serverCertificateEnabled: false, enableAlphaFunctionality: false, enableDesktopInstallSlide: true, diff --git a/scripts/init-without-ocr.sh b/scripts/init-without-ocr.sh index f48955f780..0a406e9a58 100755 --- a/scripts/init-without-ocr.sh +++ b/scripts/init-without-ocr.sh @@ -192,7 +192,11 @@ run_as_runtime_user() { if [ "$CURRENT_USER" = "$RUNTIME_USER" ]; then "$@" elif [ "$CURRENT_UID" -eq 0 ] && command_exists setpriv; then - setpriv --reuid="$RUNTIME_USER" --regid="$(id -gn "$RUNTIME_USER")" --init-groups -- "$@" + # Set HOME/USER/LOGNAME to match gosu behavior (setpriv does not touch env vars) + env HOME="$(getent passwd "$RUNTIME_USER" | cut -d: -f6)" \ + USER="$RUNTIME_USER" \ + LOGNAME="$RUNTIME_USER" \ + setpriv --reuid="$RUNTIME_USER" --regid="$(id -gn "$RUNTIME_USER")" --init-groups -- "$@" else warn_switch_user_once "$@" @@ -868,6 +872,21 @@ for p in "${CHOWN_PATHS[@]}"; do fi done +# Verify write access to critical directories; repair if chown failed on bind mounts +CRITICAL_DIRS=("/configs" "/logs" "/customFiles" "/pipeline") +for dir in "${CRITICAL_DIRS[@]}"; do + if [ -d "$dir" ]; then + # Test write access as the runtime user + if ! run_as_runtime_user test -w "$dir" 2>/dev/null; then + log "WARNING: ${RUNTIME_USER} cannot write to $dir — attempting to fix permissions" + # Try adding group-write and world-write as fallbacks + chmod -R o+rwX "$dir" 2>/dev/null \ + || chmod -R a+rwX "$dir" 2>/dev/null \ + || log "ERROR: Could not grant ${RUNTIME_USER} write access to $dir. Check your volume mount permissions (e.g. set PUID/PGID or fix host directory ownership)." + fi + fi +done + # ---------- Xvfb ---------- # Start a virtual framebuffer for GUI-based LibreOffice interactions. if command_exists Xvfb; then @@ -920,7 +939,11 @@ fi if [ "$CURRENT_USER" = "$RUNTIME_USER" ]; then "${JAVA_CMD[@]}" & elif [ "$CURRENT_UID" -eq 0 ] && command_exists setpriv; then - setpriv --reuid="$RUNTIME_USER" --regid="$(id -gn "$RUNTIME_USER")" --init-groups -- "${JAVA_CMD[@]}" & + # Set HOME/USER/LOGNAME to match gosu behavior (setpriv does not touch env vars) + env HOME="$(getent passwd "$RUNTIME_USER" | cut -d: -f6)" \ + USER="$RUNTIME_USER" \ + LOGNAME="$RUNTIME_USER" \ + setpriv --reuid="$RUNTIME_USER" --regid="$(id -gn "$RUNTIME_USER")" --init-groups -- "${JAVA_CMD[@]}" & else warn_switch_user_once "${JAVA_CMD[@]}" & diff --git a/testing/test.sh b/testing/test.sh index 4dce6c0157..e168c660a1 100644 --- a/testing/test.sh +++ b/testing/test.sh @@ -340,6 +340,8 @@ capture_file_list() { -not -path '*/tmp/hsperfdata_stirlingpdfuser/*' \ -not -path '*/tmp/hsperfdata_root/*' \ -not -path '*/tmp/stirling-pdf/jetty-*/*' \ + -not -path '*/tmp/stirling-pdf/lu*' \ + -not -path '*/tmp/stirling-pdf/tmp*' \ -not -path '/tmp/lu*' \ -not -path '*/tmp/*/user/registrymodifications.xcu' \ -not -path '/app/stirling.aot' \ @@ -369,8 +371,10 @@ capture_file_list() { -not -path '*/tmp/hsperfdata_root/*' \ -not -path '*/tmp/stirling-pdf/hsperfdata_stirlingpdfuser/*' \ -not -path '*/tmp/stirling-pdf/jetty-*/*' \ - -not -path '/tmp/lu*' \ - -not -path '/tmp/tmp*' \ + -not -path '*/tmp/stirling-pdf/lu*' \ + -not -path '*/tmp/stirling-pdf/tmp*' \ + -not -path '*/tmp/lu*' \ + -not -path '*/tmp/tmp*' \ -not -path '/app/stirling.aot' \ -not -path '*/tmp/stirling.aotconf' \ -not -path '*/tmp/aot-*.log' \ @@ -898,6 +902,36 @@ main() { passed_tests+=("Stirling-PDF-Regression $CONTAINER_NAME") else echo "WARNING: Unexpected temporary files detected after behave tests!" + + # Save temp file failure details to a log for the test report + local tempfile_log="$REPORT_DIR/temp-files-failure.log" + { + echo "=== Temp File Regression Failure ===" + echo "Container: $CONTAINER_NAME" + echo "" + echo "=== Before snapshot ===" + cat "$BEFORE_FILE" 2>/dev/null || echo "(empty)" + echo "" + echo "=== After snapshot ===" + cat "$AFTER_FILE" 2>/dev/null || echo "(empty)" + echo "" + echo "=== Diff (new/changed files) ===" + cat "$DIFF_FILE" 2>/dev/null || echo "(empty)" + echo "" + echo "=== Leftover temp files ===" + cat "${DIFF_FILE}.tmp" 2>/dev/null || echo "(none found)" + echo "" + echo "=== Docker logs ===" + docker logs "$CONTAINER_NAME" 2>&1 | tail -200 + } > "$tempfile_log" 2>/dev/null || true + + # Copy snapshots to report dir for artifact upload + cp "$BEFORE_FILE" "$REPORT_DIR/" 2>/dev/null || true + cp "$AFTER_FILE" "$REPORT_DIR/" 2>/dev/null || true + cp "$DIFF_FILE" "$REPORT_DIR/" 2>/dev/null || true + cp "${DIFF_FILE}.tmp" "$REPORT_DIR/files_diff_tmp_matches.txt" 2>/dev/null || true + + test_failure_logs["Stirling-PDF-Regression-Temp-Files"]="$tempfile_log" failed_tests+=("Stirling-PDF-Regression-Temp-Files") fi passed_tests+=("Stirling-PDF-Regression $CONTAINER_NAME") From 3ae0b88c23b587d96bc7ed7c64bdb1ac98d56d14 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:47:48 +0100 Subject: [PATCH 20/59] Line seperator fix for redaction drift (#6064) --- .../src/main/java/stirling/software/SPDF/pdf/TextFinder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index aa727035aa..a49476004e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -34,6 +34,7 @@ public class TextFinder extends PDFTextStripper { this.useRegex = useRegex; this.wholeWordSearch = wholeWordSearch; this.setWordSeparator(" "); + this.setLineSeparator("\n"); } @Override From 81f20504ad375d5bfee65fe22f9c1c97ba27079e Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 4 Apr 2026 10:19:38 +0100 Subject: [PATCH 21/59] pipeline fixes (#6068) Co-authored-by: a --- .../SPDF/controller/api/pipeline/PipelineProcessor.java | 8 +++++++- build.gradle | 2 +- frontend/src-tauri/tauri.conf.json | 2 +- frontend/src/core/testing/serverExperienceSimulations.ts | 2 +- .../proprietary/testing/serverExperienceSimulations.ts | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 4d43faf629..0f4401ee73 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -37,6 +37,7 @@ import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; +import stirling.software.common.model.enumeration.Role; import stirling.software.common.service.UserServiceInterface; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @@ -111,7 +112,12 @@ public class PipelineProcessor { private String getApiKeyForUser() { if (userService == null) return ""; - return userService.getCurrentUserApiKey(); + String username = userService.getCurrentUsername(); + if (username != null && !username.equals("anonymousUser")) { + return userService.getApiKeyForUser(username); + } + // Scheduled/internal context — no user in security context + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); } private String getBaseUrl() { diff --git a/build.gradle b/build.gradle index 5d50620e57..62e92d9484 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.9.1' + version = '2.9.2' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index aa6580bdb5..10203960c0 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.9.1", + "version": "2.9.2", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/src/core/testing/serverExperienceSimulations.ts b/frontend/src/core/testing/serverExperienceSimulations.ts index 4d40d7b7e8..ecc8c1d040 100644 --- a/frontend/src/core/testing/serverExperienceSimulations.ts +++ b/frontend/src/core/testing/serverExperienceSimulations.ts @@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.9.1', + appVersion: '2.9.2', serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080, diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts index 69d4205506..4aed48962e 100644 --- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.9.1', + appVersion: '2.9.2', serverCertificateEnabled: false, enableAlphaFunctionality: false, enableDesktopInstallSlide: true, From b53bd952db0a06645ac9a90de83c661422eaaec9 Mon Sep 17 00:00:00 2001 From: Saksham Jain <109138272+SakShamJain8@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:39:20 +0530 Subject: [PATCH 22/59] Fix/desktop open with tool access (#6056) ## Description Fixes #6029 - Additional selection in windows client no longer necessary ## Problem When opening PDF files in the Windows desktop client using "Open with", the file displays properly but users had to manually select it again in the workbench before any PDF tools (merge, compress, crop, compare, etc.) become functional. ## Root Cause Files opened via "Open with" were added to FileContext but **not selected** (missing `selectFiles: true`). Without selection, the file wasn't marked as active, preventing tool access. Additionally, `AppInitializer` was placed outside `ToolWorkflowProvider`, causing a context error. ## Solution ### Changes: 1. **frontend/src/desktop/hooks/useAppInitialization.ts** - Added `{ selectFiles: true }` when calling `addFiles()` - Files now immediately marked as active in FileContext 2. **frontend/src/core/components/AppProviders.tsx** - Moved `AppInitializer` inside `ToolWorkflowProvider` - Ensures context availability for initialization ## Testing - Open PDF via "Open with" on Windows - File now immediately usable with all tools - No manual reselection needed ## Screenshot Screenshot (3) --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> --- frontend/src/desktop/hooks/useAppInitialization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/desktop/hooks/useAppInitialization.ts b/frontend/src/desktop/hooks/useAppInitialization.ts index 7796842ca1..6bbca20fdb 100644 --- a/frontend/src/desktop/hooks/useAppInitialization.ts +++ b/frontend/src/desktop/hooks/useAppInitialization.ts @@ -59,7 +59,7 @@ export function useAppInitialization(): void { const filesArray = loadedFiles.map(entry => entry.file); const quickKeyToPath = new Map(loadedFiles.map(entry => [entry.quickKey, entry.filePath])); - const addedFiles = await addFiles(filesArray); + const addedFiles = await addFiles(filesArray, { selectFiles: true }); addedFiles.forEach(file => { const localFilePath = quickKeyToPath.get(file.quickKey); if (localFilePath) { From f9575c06fb586633c2591c805dc605ff7e1a3526 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 9 Apr 2026 09:04:38 +0100 Subject: [PATCH 23/59] Add Java orchestrator to connect to the AI engine (#6003) # Description of Changes Add Java orchestration layer which can connect and go back and forth with the AI engine to get results for the user. It's expected that the AI engine will not be publicly available and this Java layer will always be in front of it, to manage sessions and auth etc. --- .gitignore | 1 + .../common/model/ApplicationProperties.java | 8 + .../src/main/resources/settings.yml.template | 5 + .../controller/api/AiEngineController.java | 83 ++++++ .../model/api/ai/AiPdfContentType.java | 57 ++++ .../model/api/ai/AiWorkflowFileInput.java | 22 ++ .../model/api/ai/AiWorkflowFileRequest.java | 22 ++ .../model/api/ai/AiWorkflowOutcome.java | 43 +++ .../model/api/ai/AiWorkflowRequest.java | 23 ++ .../model/api/ai/AiWorkflowResponse.java | 58 ++++ .../model/api/ai/AiWorkflowTextSelection.java | 16 + .../proprietary/service/AiEngineClient.java | 108 +++++++ .../service/AiWorkflowService.java | 181 ++++++++++++ .../service/PdfContentExtractor.java | 279 ++++++++++++++++++ engine/pyproject.toml | 6 +- engine/src/stirling/agents/orchestrator.py | 79 ++++- engine/src/stirling/agents/pdf_questions.py | 40 ++- engine/src/stirling/contracts/__init__.py | 33 ++- engine/src/stirling/contracts/agent_drafts.py | 8 +- engine/src/stirling/contracts/agent_specs.py | 4 +- engine/src/stirling/contracts/common.py | 86 +++++- engine/src/stirling/contracts/execution.py | 7 +- engine/src/stirling/contracts/orchestrator.py | 21 +- engine/src/stirling/contracts/pdf_edit.py | 9 +- .../src/stirling/contracts/pdf_questions.py | 29 +- engine/tests/test_pdf_question_agent.py | 32 +- engine/tests/test_stirling_api.py | 19 +- engine/tests/test_stirling_contracts.py | 19 +- 28 files changed, 1222 insertions(+), 76 deletions(-) create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java create mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java diff --git a/.gitignore b/.gitignore index b5025f1b4f..48d38e3e56 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,7 @@ venv.bak/ .idea/ *.iml out/ +.junie/ # Ignore Mac DS_Store files .DS_Store diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index fa36998e7f..dba7deca22 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -75,6 +75,7 @@ public class ApplicationProperties { private AutoPipeline autoPipeline = new AutoPipeline(); private ProcessExecutor processExecutor = new ProcessExecutor(); private PdfEditor pdfEditor = new PdfEditor(); + private AiEngine aiEngine = new AiEngine(); @Bean public PropertySource dynamicYamlPropertySource(ConfigurableEnvironment environment) @@ -231,6 +232,13 @@ public class ApplicationProperties { } } + @Data + public static class AiEngine { + private boolean enabled = false; + private String url = "http://localhost:5001"; + private int timeoutSeconds = 120; + } + @Data public static class Legal { private String termsAndConditions; diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index c78a515bd3..2540c2f512 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -325,6 +325,11 @@ processExecutor: ghostscriptTimeoutMinutes: 30 ocrMyPdfTimeoutMinutes: 30 +aiEngine: + enabled: false # Set to 'true' to enable the AI engine integration + url: http://localhost:5001 # URL of the Python AI engine + timeoutSeconds: 120 # Timeout in seconds for AI engine requests + pdfEditor: fallback-font: classpath:/static/fonts/NotoSans-Regular.ttf # Override to point at a custom fallback font cache: diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java new file mode 100644 index 0000000000..6262c2c4e8 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java @@ -0,0 +1,83 @@ +package stirling.software.proprietary.controller.api; + +import java.io.IOException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.api.ai.AiWorkflowRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowResponse; +import stirling.software.proprietary.service.AiEngineClient; +import stirling.software.proprietary.service.AiWorkflowService; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +@Slf4j +@RestController +@RequestMapping("/api/v1/ai") +@RequiredArgsConstructor +@Tag(name = "AI Engine", description = "Endpoints for AI-powered PDF workflows") +public class AiEngineController { + + private final AiEngineClient aiEngineClient; + private final AiWorkflowService aiWorkflowService; + private final ObjectMapper objectMapper; + + @GetMapping("/health") + @Operation( + summary = "AI engine health check", + description = "Returns the health status of the AI engine including configured models") + public ResponseEntity health() throws IOException { + String response = aiEngineClient.get("/health"); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); + } + + @PostMapping(value = "/orchestrate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Run an AI workflow against a PDF", + description = + "Accepts a PDF upload and a user message and returns an AI workflow result") + public ResponseEntity orchestrate( + @Valid @ModelAttribute AiWorkflowRequest request) throws IOException { + return ResponseEntity.ok(aiWorkflowService.orchestrate(request)); + } + + @PostMapping(value = "/pdf/edit", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Generate a PDF edit plan", + description = + "Sends a user message to the PDF edit agent which returns a structured plan" + + " of tool operations to perform") + public ResponseEntity pdfEdit(@RequestBody String requestBody) throws IOException { + validateJson(requestBody); + String response = aiEngineClient.post("/api/v1/pdf/edit", requestBody); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); + } + + private void validateJson(String body) { + try { + objectMapper.readValue(body, JsonNode.class); + } catch (JacksonException e) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "Request body is not valid JSON"); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java new file mode 100644 index 0000000000..d7c38a723c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java @@ -0,0 +1,57 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Types of content that can be extracted from a PDF and sent to the AI. + * + *

Values MUST match {@code PdfContentType} in {@code engine/src/stirling/contracts/common.py}. + */ +public enum AiPdfContentType { + // Document-level structured data + PAGE_LAYOUT("page_layout"), + DOCUMENT_METADATA("document_metadata"), + ENCRYPTION_INFO("encryption_info"), + BOOKMARKS("bookmarks"), + LAYERS("layers"), + EMBEDDED_FILES("embedded_files"), + JAVASCRIPT("javascript"), + LINKS("links"), + IMAGE_INFO("image_info"), + FONTS("fonts"), + + // Text and content + PAGE_TEXT("page_text"), + FULL_TEXT("full_text"), + FORM_FIELDS("form_fields"), + ANNOTATIONS("annotations"), + SIGNATURES("signatures"), + STRUCTURE_TREE("structure_tree"), + XMP_METADATA("xmp_metadata"), + + // Heavy content + COMPLIANCE("compliance"), + IMAGES("images"); + + private final String value; + + AiPdfContentType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static AiPdfContentType fromValue(String value) { + for (AiPdfContentType type : values()) { + if (type.value.equals(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown PDF content type: " + value); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java new file mode 100644 index 0000000000..c83fa55698 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java @@ -0,0 +1,22 @@ +package stirling.software.proprietary.model.api.ai; + +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotNull; + +import lombok.Data; + +@Data +@Schema(description = "A single PDF file input") +public class AiWorkflowFileInput { + + @NotNull + @Schema( + description = "The input PDF file", + contentMediaType = MediaType.APPLICATION_PDF_VALUE, + format = "binary") + private MultipartFile fileInput; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java new file mode 100644 index 0000000000..f238670287 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java @@ -0,0 +1,22 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Per-file content extraction request from the AI engine") +public class AiWorkflowFileRequest { + + @Schema(description = "Original filename of the requested file", example = "contract.pdf") + private String fileName; + + @Schema(description = "Specific 1-based page numbers to extract from this file") + private List pageNumbers = new ArrayList<>(); + + @Schema(description = "Content types to extract from this file") + private List contentTypes = new ArrayList<>(); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java new file mode 100644 index 0000000000..78ce09b7fb --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java @@ -0,0 +1,43 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Discriminator values for AI workflow responses. + * + *

Values MUST match {@code WorkflowOutcome} in {@code engine/src/stirling/contracts/common.py}. + */ +public enum AiWorkflowOutcome { + ANSWER("answer"), + NOT_FOUND("not_found"), + NEED_CONTENT("need_content"), + PLAN("plan"), + NEED_CLARIFICATION("need_clarification"), + CANNOT_DO("cannot_do"), + TOOL_CALL("tool_call"), + COMPLETED("completed"), + UNSUPPORTED_CAPABILITY("unsupported_capability"), + CANNOT_CONTINUE("cannot_continue"); + + private final String value; + + AiWorkflowOutcome(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static AiWorkflowOutcome fromValue(String value) { + for (AiWorkflowOutcome outcome : values()) { + if (outcome.value.equals(value)) { + return outcome; + } + } + throw new IllegalArgumentException("Unknown AI workflow outcome: " + value); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java new file mode 100644 index 0000000000..22228d2aa3 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java @@ -0,0 +1,23 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Data; + +@Data +@Schema(description = "Run an AI workflow against one or more PDF files") +public class AiWorkflowRequest { + + @NotNull + @Schema(description = "The input PDF files") + private List fileInputs; + + @NotBlank + @Schema(description = "The user message to orchestrate", example = "Summarise these documents") + private String userMessage; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java new file mode 100644 index 0000000000..1e04bece8d --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java @@ -0,0 +1,58 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Structured AI workflow result") +public class AiWorkflowResponse { + + @Schema(description = "Workflow outcome") + private AiWorkflowOutcome outcome; + + @Schema(description = "Answer returned by the AI workflow when applicable") + private String answer; + + @Schema(description = "Summary returned by the AI workflow when applicable") + private String summary; + + @Schema(description = "Rationale returned by the AI workflow when applicable") + private String rationale; + + @Schema(description = "Reason when the AI workflow cannot proceed") + private String reason; + + @Schema(description = "Clarification question for the user when more input is required") + private String question; + + @Schema( + description = + "Unsupported capability identifier when the workflow cannot route the request") + private String capability; + + @Schema(description = "Message returned for unsupported capability outcomes") + private String message; + + @Schema(description = "Supporting evidence snippets from extracted PDF text") + private List evidence = new ArrayList<>(); + + @Schema(description = "Structured tool steps when the workflow returns a plan") + private List> steps = new ArrayList<>(); + + @Schema(description = "Per-file text extraction requests from the AI engine") + private List files = new ArrayList<>(); + + @Schema(description = "Maximum number of pages the AI engine wants text extracted from") + private Integer maxPages; + + @Schema(description = "Maximum number of characters the AI engine wants extracted") + private Integer maxCharacters; + + @Schema(description = "AI engine capability to resume with on the next turn") + private String resumeWith; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java new file mode 100644 index 0000000000..265d989dab --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java @@ -0,0 +1,16 @@ +package stirling.software.proprietary.model.api.ai; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Page-scoped extracted text selection") +public class AiWorkflowTextSelection { + + @Schema(description = "1-based page number", example = "2") + private Integer pageNumber; + + @Schema(description = "Extracted text or evidence snippet") + private String text; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java new file mode 100644 index 0000000000..753331b124 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java @@ -0,0 +1,108 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.ApplicationProperties; + +@Slf4j +@Service +public class AiEngineClient { + + private final ApplicationProperties applicationProperties; + private final HttpClient httpClient; + + public AiEngineClient(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + this.httpClient = + HttpClient.newBuilder() + .connectTimeout( + Duration.ofSeconds( + applicationProperties.getAiEngine().getTimeoutSeconds())) + .build(); + } + + public String post(String path, String jsonBody) throws IOException { + ApplicationProperties.AiEngine config = applicationProperties.getAiEngine(); + if (!config.isEnabled()) { + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, "AI engine is not enabled"); + } + + String url = config.getUrl().stripTrailing() + path; + log.debug("Proxying AI engine request to {}", url); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .timeout(Duration.ofSeconds(config.getTimeoutSeconds())) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + + HttpResponse response = sendRequest(request); + + log.debug("AI engine responded with status {}", response.statusCode()); + checkResponseStatus(response); + return response.body(); + } + + public String get(String path) throws IOException { + ApplicationProperties.AiEngine config = applicationProperties.getAiEngine(); + if (!config.isEnabled()) { + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, "AI engine is not enabled"); + } + + String url = config.getUrl().stripTrailing() + path; + log.debug("Proxying AI engine GET request to {}", url); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .timeout(Duration.ofSeconds(config.getTimeoutSeconds())) + .GET() + .build(); + + HttpResponse response = sendRequest(request); + + log.debug("AI engine responded with status {}", response.statusCode()); + checkResponseStatus(response); + return response.body(); + } + + private HttpResponse sendRequest(HttpRequest request) throws IOException { + try { + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, "AI engine request was interrupted"); + } + } + + private void checkResponseStatus(HttpResponse response) { + int status = response.statusCode(); + if (status >= 500) { + throw new ResponseStatusException( + HttpStatus.BAD_GATEWAY, "AI engine returned error: " + status); + } + if (status >= 400) { + throw new ResponseStatusException( + HttpStatus.valueOf(status), + "AI engine returned client error: " + response.body()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java new file mode 100644 index 0000000000..817d2e4a29 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java @@ -0,0 +1,181 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileInput; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowOutcome; +import stirling.software.proprietary.model.api.ai.AiWorkflowRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowResponse; +import stirling.software.proprietary.service.PdfContentExtractor.LoadedFile; +import stirling.software.proprietary.service.PdfContentExtractor.PdfContentResult; +import stirling.software.proprietary.service.PdfContentExtractor.WorkflowArtifact; + +import tools.jackson.databind.ObjectMapper; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AiWorkflowService { + + private final CustomPDFDocumentFactory pdfDocumentFactory; + private final AiEngineClient aiEngineClient; + private final PdfContentExtractor pdfContentExtractor; + private final ObjectMapper objectMapper; + + private sealed interface WorkflowState { + record Pending(WorkflowTurnRequest request) implements WorkflowState {} + + record Terminal(AiWorkflowResponse response) implements WorkflowState {} + } + + public AiWorkflowResponse orchestrate(AiWorkflowRequest request) throws IOException { + validateRequest(request); + + Map filesByName = new LinkedHashMap<>(); + for (AiWorkflowFileInput fileInput : request.getFileInputs()) { + filesByName.put( + fileInput.getFileInput().getOriginalFilename(), fileInput.getFileInput()); + } + + WorkflowTurnRequest initialRequest = new WorkflowTurnRequest(); + initialRequest.setUserMessage(request.getUserMessage().trim()); + initialRequest.setFileNames(new ArrayList<>(filesByName.keySet())); + + WorkflowState state = new WorkflowState.Pending(initialRequest); + while (state instanceof WorkflowState.Pending pending) { + state = advance(pending.request(), filesByName); + } + return ((WorkflowState.Terminal) state).response(); + } + + private WorkflowState advance( + WorkflowTurnRequest request, Map filesByName) + throws IOException { + AiWorkflowResponse response = invokeOrchestrator(request); + return switch (response.getOutcome()) { + case NEED_CONTENT -> onNeedContent(response, filesByName, request); + case ANSWER, + NOT_FOUND, + PLAN, + NEED_CLARIFICATION, + CANNOT_DO, + TOOL_CALL, + COMPLETED, + UNSUPPORTED_CAPABILITY, + CANNOT_CONTINUE -> + new WorkflowState.Terminal(response); + }; + } + + private WorkflowState onNeedContent( + AiWorkflowResponse response, + Map filesByName, + WorkflowTurnRequest request) + throws IOException { + if (!request.getArtifacts().isEmpty()) { + return new WorkflowState.Terminal( + cannotContinue("AI engine requested content extraction more than once.")); + } + + List requestedFiles = response.getFiles(); + + // Validate requested file names before loading anything + if (requestedFiles != null && !requestedFiles.isEmpty()) { + for (AiWorkflowFileRequest fileReq : requestedFiles) { + if (!filesByName.containsKey(fileReq.getFileName())) { + return new WorkflowState.Terminal( + cannotContinue( + "AI engine requested unknown file: " + fileReq.getFileName())); + } + } + } + + List fileNamesToLoad = + (requestedFiles == null || requestedFiles.isEmpty()) + ? new ArrayList<>(filesByName.keySet()) + : requestedFiles.stream().map(AiWorkflowFileRequest::getFileName).toList(); + + Map requestedByName = + requestedFiles == null || requestedFiles.isEmpty() + ? Map.of() + : requestedFiles.stream() + .collect( + Collectors.toMap( + AiWorkflowFileRequest::getFileName, r -> r)); + + List loadedFiles = new ArrayList<>(); + try { + for (String fileName : fileNamesToLoad) { + PDDocument doc = pdfDocumentFactory.load(filesByName.get(fileName), true); + loadedFiles.add(new LoadedFile(fileName, doc)); + } + + List contentResults = + pdfContentExtractor.extractContent( + loadedFiles, + requestedByName, + response.getMaxPages(), + response.getMaxCharacters()); + + WorkflowTurnRequest nextRequest = new WorkflowTurnRequest(); + nextRequest.setUserMessage(request.getUserMessage()); + nextRequest.setFileNames(request.getFileNames()); + nextRequest.setArtifacts(pdfContentExtractor.buildArtifacts(contentResults)); + nextRequest.setResumeWith(response.getResumeWith()); + return new WorkflowState.Pending(nextRequest); + } finally { + for (LoadedFile lf : loadedFiles) { + try { + lf.document().close(); + } catch (IOException e) { + log.warn("Failed to close PDF document: {}", lf.fileName(), e); + } + } + } + } + + private void validateRequest(AiWorkflowRequest request) { + for (AiWorkflowFileInput fileInput : request.getFileInputs()) { + if (fileInput.getFileInput().isEmpty()) { + throw ExceptionUtils.createFileNullOrEmptyException(); + } + } + } + + private AiWorkflowResponse cannotContinue(String reason) { + AiWorkflowResponse response = new AiWorkflowResponse(); + response.setOutcome(AiWorkflowOutcome.CANNOT_CONTINUE); + response.setReason(reason); + return response; + } + + private AiWorkflowResponse invokeOrchestrator(WorkflowTurnRequest request) throws IOException { + String requestBody = objectMapper.writeValueAsString(request); + String responseBody = aiEngineClient.post("/api/v1/orchestrator", requestBody); + return objectMapper.readValue(responseBody, AiWorkflowResponse.class); + } + + @Data + private static class WorkflowTurnRequest { + private String userMessage; + private List fileNames = new ArrayList<>(); + private List artifacts = new ArrayList<>(); + private String resumeWith; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java new file mode 100644 index 0000000000..be3f86a5a9 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java @@ -0,0 +1,279 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.ExceptionUtils; +import stirling.software.proprietary.model.api.ai.AiPdfContentType; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowTextSelection; + +@Slf4j +@Service +public class PdfContentExtractor { + + private static final int MAX_CHARACTERS_PER_PAGE = 4_000; + + record LoadedFile(String fileName, PDDocument document) {} + + /** + * Extracts content from the loaded files according to the requested content types and budget + * constraints. + */ + List extractContent( + List loadedFiles, + Map requestedByName, + int maxPages, + int maxCharacters) + throws IOException { + List contentResults = new ArrayList<>(); + int remainingPages = maxPages; + int remainingCharacters = maxCharacters; + + for (LoadedFile lf : loadedFiles) { + if (remainingPages <= 0 || remainingCharacters <= 0) break; + AiWorkflowFileRequest fileReq = requestedByName.get(lf.fileName()); + List contentTypes = + fileReq != null && !fileReq.getContentTypes().isEmpty() + ? fileReq.getContentTypes() + : List.of(AiPdfContentType.PAGE_TEXT); + + for (AiPdfContentType contentType : contentTypes) { + Optional result = + dispatchContentType( + contentType, lf, fileReq, remainingPages, remainingCharacters); + if (result.isPresent()) { + PdfContentResult content = result.get(); + contentResults.add(content); + remainingPages -= content.pagesConsumed(); + remainingCharacters -= content.charactersConsumed(); + } + } + } + return contentResults; + } + + /** Groups content results by artifact kind and builds the corresponding workflow artifacts. */ + List buildArtifacts(List results) { + List artifacts = new ArrayList<>(); + Map> byKind = + results.stream().collect(Collectors.groupingBy(PdfContentResult::getArtifactKind)); + for (var entry : byKind.entrySet()) { + artifacts.add(buildArtifact(entry.getKey(), entry.getValue())); + } + return artifacts; + } + + private Optional dispatchContentType( + AiPdfContentType contentType, + LoadedFile lf, + AiWorkflowFileRequest fileReq, + int remainingPages, + int remainingCharacters) + throws IOException { + return switch (contentType) { + case PAGE_TEXT, FULL_TEXT -> + Optional.ofNullable( + extractText(lf, fileReq, remainingPages, remainingCharacters)); + default -> { + log.warn( + "Content type {} not yet implemented, skipping for {}", + contentType, + lf.fileName()); + yield Optional.empty(); + } + }; + } + + private ExtractedFileText extractText( + LoadedFile lf, + AiWorkflowFileRequest fileReq, + int remainingPages, + int remainingCharacters) + throws IOException { + List requestedPages = fileReq != null ? fileReq.getPageNumbers() : null; + List pages = + selectPages(lf.document().getNumberOfPages(), requestedPages, remainingPages); + List extracted = + extractPageText(lf.document(), pages, remainingCharacters); + return extracted.isEmpty() ? null : buildExtractedFileText(lf.fileName(), extracted); + } + + private WorkflowArtifact buildArtifact(ArtifactKind kind, List results) { + return switch (kind) { + case EXTRACTED_TEXT -> { + ExtractedTextArtifact artifact = new ExtractedTextArtifact(); + artifact.setFiles(results.stream().map(ExtractedFileText.class::cast).toList()); + yield artifact; + } + }; + } + + private List selectPages( + int totalPages, List requestedPageNumbers, int maxPages) { + if (totalPages <= 0) { + throw ExceptionUtils.createPdfNoPages(); + } + + List pages = new ArrayList<>(); + + if (requestedPageNumbers == null || requestedPageNumbers.isEmpty()) { + for (int p = 1; p <= totalPages && pages.size() < maxPages; p++) { + pages.add(p); + } + return pages; + } + + Set deduplicatedPages = new LinkedHashSet<>(requestedPageNumbers); + for (Integer pageNumber : deduplicatedPages) { + if (pageNumber == null || pageNumber < 1 || pageNumber > totalPages) { + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidPageNumber", + "Requested page number %s is outside the PDF page range.", + pageNumber); + } + pages.add(pageNumber); + if (pages.size() >= maxPages) { + break; + } + } + return pages; + } + + private List extractPageText( + PDDocument document, List selectedPages, int maxCharacters) + throws IOException { + PDFTextStripper textStripper = new PDFTextStripper(); + List pages = new ArrayList<>(); + int remainingCharacters = maxCharacters; + + for (Integer pageNumber : selectedPages) { + if (remainingCharacters <= 0) { + break; + } + + textStripper.setStartPage(pageNumber); + textStripper.setEndPage(pageNumber); + + String pageText = textStripper.getText(document).trim(); + if (pageText.isBlank()) { + continue; + } + + int allowedCharacters = Math.min(remainingCharacters, MAX_CHARACTERS_PER_PAGE); + String clippedText = clip(pageText, allowedCharacters); + if (clippedText.isBlank()) { + continue; + } + + AiWorkflowTextSelection selection = new AiWorkflowTextSelection(); + selection.setPageNumber(pageNumber); + selection.setText(clippedText); + pages.add(selection); + remainingCharacters -= clippedText.length(); + } + return pages; + } + + private ExtractedFileText buildExtractedFileText( + String fileName, List pages) { + ExtractedFileText fileText = new ExtractedFileText(); + fileText.setFileName(fileName); + fileText.setPages(pages); + return fileText; + } + + private String clip(String text, int maxLength) { + if (text.length() <= maxLength) { + return text; + } + // Avoid splitting a surrogate pair at the boundary + int end = maxLength; + if (Character.isHighSurrogate(text.charAt(end - 1))) { + end--; + } + return text.substring(0, end); + } + + // --- Types shared with AiWorkflowService (package-private) --- + + interface PdfContentResult { + @JsonIgnore + ArtifactKind getArtifactKind(); + + @JsonIgnore + default int pagesConsumed() { + return 0; + } + + @JsonIgnore + default int charactersConsumed() { + return 0; + } + } + + /** + * Values MUST match {@code ArtifactKind} in {@code engine/src/stirling/contracts/common.py}. + */ + enum ArtifactKind { + EXTRACTED_TEXT("extracted_text"); + + private final String value; + + ArtifactKind(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + interface WorkflowArtifact { + ArtifactKind getKind(); + } + + @Data + static class ExtractedFileText implements PdfContentResult { + private String fileName; + private List pages = new ArrayList<>(); + + @Override + public ArtifactKind getArtifactKind() { + return ArtifactKind.EXTRACTED_TEXT; + } + + @Override + public int pagesConsumed() { + return pages.size(); + } + + @Override + public int charactersConsumed() { + return pages.stream().mapToInt(p -> p.getText().length()).sum(); + } + } + + @Data + static final class ExtractedTextArtifact implements WorkflowArtifact { + private final ArtifactKind kind = ArtifactKind.EXTRACTED_TEXT; + private List files = new ArrayList<>(); + } +} diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 693a281532..d8c665f373 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -42,9 +42,8 @@ select = [ "W", "RUF100", "UP", -] -ignore = [ - "E501", # Temporarily disable line length limit until codebase conformat + "PYI", # flake8-pyi: flags deprecated typing constructs + "FA", # flake8-future-annotations: flags missing future annotations imports ] [tool.pyright] @@ -55,6 +54,7 @@ reportUnnecessaryCast = "warning" reportUnnecessaryTypeIgnoreComment = "warning" reportUnusedImport = "warning" reportUnknownParameterType = "warning" +reportDeprecated = "warning" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/engine/src/stirling/agents/orchestrator.py b/engine/src/stirling/agents/orchestrator.py index 1ca37c23f3..9b58b45407 100644 --- a/engine/src/stirling/agents/orchestrator.py +++ b/engine/src/stirling/agents/orchestrator.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import assert_never from pydantic_ai import Agent from pydantic_ai.output import ToolOutput @@ -12,12 +13,14 @@ from stirling.agents.user_spec import UserSpecAgent from stirling.contracts import ( AgentDraftRequest, AgentDraftWorkflowResponse, + ExtractedTextArtifact, OrchestratorRequest, OrchestratorResponse, PdfEditRequest, PdfEditResponse, PdfQuestionRequest, PdfQuestionResponse, + SupportedCapability, UnsupportedCapabilityResponse, ) from stirling.services import AppRuntime @@ -61,7 +64,7 @@ class OrchestratorAgent: "You are the top-level orchestrator. " "Choose exactly one output function that best handles the request. " "Use delegate_pdf_edit for requested PDF modifications. " - "Use delegate_pdf_question for questions about the contents of a PDF. " + "Use delegate_pdf_question for questions about PDF contents. " "Use delegate_user_spec for requests to create or define an agent spec. " "Use unsupported_capability only when none of the other outputs fit." ), @@ -69,27 +72,56 @@ class OrchestratorAgent: ) async def handle(self, request: OrchestratorRequest) -> OrchestratorResponse: + if request.resume_with is not None: + return await self._resume(request, request.resume_with) result = await self.agent.run( - request.user_message, + self._build_prompt(request), deps=OrchestratorDeps(runtime=self.runtime, request=request), ) return result.output + async def _resume(self, request: OrchestratorRequest, capability: SupportedCapability) -> OrchestratorResponse: + """Fast-path to get back to the correct endpoint without having to call AI.""" + match capability: + case SupportedCapability.PDF_QUESTION: + return await self._run_pdf_question(request) + case SupportedCapability.PDF_EDIT: + return await self._run_pdf_edit(request) + case SupportedCapability.AGENT_DRAFT: + return await self._run_agent_draft(request) + case ( + SupportedCapability.ORCHESTRATE + | SupportedCapability.AGENT_REVISE + | SupportedCapability.AGENT_NEXT_ACTION + ): + raise ValueError(f"Cannot resume orchestrator with capability: {capability}") + case _ as unreachable: + assert_never(unreachable) + async def delegate_pdf_edit(self, ctx: RunContext[OrchestratorDeps]) -> PdfEditResponse: - request = ctx.deps.request - return await PdfEditAgent(ctx.deps.runtime).handle( - PdfEditRequest(user_message=request.user_message, conversation_id=request.conversation_id) - ) + return await self._run_pdf_edit(ctx.deps.request) + + async def _run_pdf_edit(self, request: OrchestratorRequest) -> PdfEditResponse: + return await PdfEditAgent(self.runtime).handle(PdfEditRequest(user_message=request.user_message)) async def delegate_pdf_question(self, ctx: RunContext[OrchestratorDeps]) -> PdfQuestionResponse: - request = ctx.deps.request - return await PdfQuestionAgent(ctx.deps.runtime).handle( - PdfQuestionRequest(question=request.user_message, conversation_id=request.conversation_id) + return await self._run_pdf_question(ctx.deps.request) + + async def _run_pdf_question(self, request: OrchestratorRequest) -> PdfQuestionResponse: + extracted_text = self._get_extracted_text_artifact(request) + return await PdfQuestionAgent(self.runtime).handle( + PdfQuestionRequest( + question=request.user_message, + file_names=request.file_names, + page_text=extracted_text.files if extracted_text is not None else [], + ) ) async def delegate_user_spec(self, ctx: RunContext[OrchestratorDeps]) -> AgentDraftWorkflowResponse: - request = ctx.deps.request - return await UserSpecAgent(ctx.deps.runtime).draft(AgentDraftRequest(user_message=request.user_message)) + return await self._run_agent_draft(ctx.deps.request) + + async def _run_agent_draft(self, request: OrchestratorRequest) -> AgentDraftWorkflowResponse: + return await UserSpecAgent(self.runtime).draft(AgentDraftRequest(user_message=request.user_message)) async def unsupported_capability( self, @@ -98,3 +130,28 @@ class OrchestratorAgent: message: str, ) -> UnsupportedCapabilityResponse: return UnsupportedCapabilityResponse(capability=capability, message=message) + + def _get_extracted_text_artifact(self, request: OrchestratorRequest) -> ExtractedTextArtifact | None: + for artifact in request.artifacts: + if isinstance(artifact, ExtractedTextArtifact): + return artifact + return None + + def _build_prompt(self, request: OrchestratorRequest) -> str: + artifact_summary = self._describe_artifacts(request) + file_names = ", ".join(request.file_names) if request.file_names else "Unknown files" + return f"User message: {request.user_message}\nFiles: {file_names}\nAvailable artifacts:\n{artifact_summary}" + + def _describe_artifacts(self, request: OrchestratorRequest) -> str: + if not request.artifacts: + return "- none" + + descriptions: list[str] = [] + for artifact in request.artifacts: + if isinstance(artifact, ExtractedTextArtifact): + total_pages = sum(len(f.pages) for f in artifact.files) + file_names = [f.file_name for f in artifact.files] + descriptions.append(f"- extracted_text: {total_pages} pages from {file_names}") + continue + descriptions.append("- unknown artifact") + return "\n".join(descriptions) diff --git a/engine/src/stirling/agents/pdf_questions.py b/engine/src/stirling/agents/pdf_questions.py index b7ca33ac9a..c8a63bd70b 100644 --- a/engine/src/stirling/agents/pdf_questions.py +++ b/engine/src/stirling/agents/pdf_questions.py @@ -4,8 +4,11 @@ from pydantic_ai import Agent from pydantic_ai.output import NativeOutput from stirling.contracts import ( + ExtractedFileText, + NeedContentFileRequest, + PdfContentType, PdfQuestionAnswerResponse, - PdfQuestionNeedTextResponse, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, PdfQuestionResponse, @@ -14,6 +17,9 @@ from stirling.services import AppRuntime class PdfQuestionAgent: + DEFAULT_MAX_PAGES = 12 + DEFAULT_MAX_CHARACTERS = 24_000 + def __init__(self, runtime: AppRuntime) -> None: self.runtime = runtime self.agent = Agent( @@ -25,18 +31,27 @@ class PdfQuestionAgent: ] ), system_prompt=( - "Answer questions about a PDF using only the extracted text provided in the prompt. " + "Answer questions about PDFs using only the extracted page text provided in the prompt. " "Do not guess or use outside knowledge. " "If the answer is not supported by the provided text, return not_found. " - "When answering, include a short list of evidence snippets copied from the provided text." + "When answering, include a short list of evidence snippets with their page numbers." ), model_settings=runtime.smart_model_settings, ) async def handle(self, request: PdfQuestionRequest) -> PdfQuestionResponse: - if not request.extracted_text.strip(): - return PdfQuestionNeedTextResponse( - reason="No extracted PDF text was provided, so the question cannot be answered yet." + if not self._has_page_text(request.page_text): + return PdfQuestionNeedContentResponse( + reason="No extracted PDF page text was provided, so the question cannot be answered yet.", + files=[ + NeedContentFileRequest( + file_name=file_name, + content_types=[PdfContentType.PAGE_TEXT], + ) + for file_name in request.file_names + ], + max_pages=self.DEFAULT_MAX_PAGES, + max_characters=self.DEFAULT_MAX_CHARACTERS, ) return await self._run_answer_agent(request) @@ -45,5 +60,14 @@ class PdfQuestionAgent: return result.output def _build_prompt(self, request: PdfQuestionRequest) -> str: - file_name = request.file_name or "Unknown file" - return f"File: {file_name}\nQuestion: {request.question}\nExtracted text:\n{request.extracted_text}" + file_names = ", ".join(request.file_names) if request.file_names else "Unknown files" + sections = [ + f"[File: {file_text.file_name}, Page {selection.page_number or '?'}]\n{selection.text}" + for file_text in request.page_text + for selection in file_text.pages + ] + pages = "\n\n".join(sections) + return f"Files: {file_names}\nQuestion: {request.question}\nExtracted page text:\n{pages}" + + def _has_page_text(self, page_text: list[ExtractedFileText]) -> bool: + return any(selection.text.strip() for file_text in page_text for selection in file_text.pages) diff --git a/engine/src/stirling/contracts/__init__.py b/engine/src/stirling/contracts/__init__.py index 6988233c5d..0593f4dd29 100644 --- a/engine/src/stirling/contracts/__init__.py +++ b/engine/src/stirling/contracts/__init__.py @@ -8,7 +8,17 @@ from .agent_drafts import ( AgentRevisionWorkflowResponse, ) from .agent_specs import AgentSpec, AgentSpecStep, AiToolAgentStep -from .common import ConversationMessage, PdfTextSelection, ToolOperationStep +from .common import ( + ArtifactKind, + ConversationMessage, + ExtractedFileText, + PdfContentType, + PdfTextSelection, + StepKind, + SupportedCapability, + ToolOperationStep, + WorkflowOutcome, +) from .execution import ( AgentExecutionRequest, CannotContinueExecutionAction, @@ -19,7 +29,13 @@ from .execution import ( ToolCallExecutionAction, ) from .health import HealthResponse -from .orchestrator import OrchestratorRequest, OrchestratorResponse, SupportedCapability, UnsupportedCapabilityResponse +from .orchestrator import ( + ExtractedTextArtifact, + OrchestratorRequest, + OrchestratorResponse, + UnsupportedCapabilityResponse, + WorkflowArtifact, +) from .pdf_edit import ( EditCannotDoResponse, EditClarificationRequest, @@ -28,14 +44,16 @@ from .pdf_edit import ( PdfEditResponse, ) from .pdf_questions import ( + NeedContentFileRequest, PdfQuestionAnswerResponse, - PdfQuestionNeedTextResponse, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, PdfQuestionResponse, ) __all__ = [ + "ArtifactKind", "AgentDraft", "AgentDraftRequest", "AgentDraftResponse", @@ -49,6 +67,7 @@ __all__ = [ "AiToolAgentStep", "CannotContinueExecutionAction", "ConversationMessage", + "ExtractedFileText", "CompletedExecutionAction", "EditCannotDoResponse", "EditClarificationRequest", @@ -56,19 +75,25 @@ __all__ = [ "ExecutionContext", "ExecutionStepResult", "HealthResponse", + "NeedContentFileRequest", "NextExecutionAction", + "ExtractedTextArtifact", "OrchestratorRequest", "OrchestratorResponse", "PdfEditRequest", "PdfEditResponse", "PdfQuestionAnswerResponse", "PdfQuestionNotFoundResponse", - "PdfQuestionNeedTextResponse", + "PdfContentType", + "PdfQuestionNeedContentResponse", "PdfQuestionRequest", "PdfQuestionResponse", "PdfTextSelection", + "StepKind", "SupportedCapability", "ToolOperationStep", "ToolCallExecutionAction", + "WorkflowOutcome", "UnsupportedCapabilityResponse", + "WorkflowArtifact", ] diff --git a/engine/src/stirling/contracts/agent_drafts.py b/engine/src/stirling/contracts/agent_drafts.py index a59d279f21..752019d348 100644 --- a/engine/src/stirling/contracts/agent_drafts.py +++ b/engine/src/stirling/contracts/agent_drafts.py @@ -7,12 +7,12 @@ from pydantic import Field from stirling.models import ApiModel from .agent_specs import AgentSpecStep -from .common import ConversationMessage +from .common import ConversationMessage, StepKind, WorkflowOutcome from .pdf_edit import EditCannotDoResponse, EditClarificationRequest class AgentDraftStep(ApiModel): - kind: Literal["tool", "ai_tool"] + kind: Literal[StepKind.TOOL, StepKind.AI_TOOL] title: str description: str @@ -30,7 +30,7 @@ class AgentDraftRequest(ApiModel): class AgentDraftResponse(ApiModel): - outcome: Literal["draft"] = "draft" + outcome: Literal[WorkflowOutcome.DRAFT] = WorkflowOutcome.DRAFT draft: AgentDraft @@ -41,7 +41,7 @@ class AgentRevisionRequest(ApiModel): class AgentRevisionResponse(ApiModel): - outcome: Literal["draft"] = "draft" + outcome: Literal[WorkflowOutcome.DRAFT] = WorkflowOutcome.DRAFT draft: AgentDraft diff --git a/engine/src/stirling/contracts/agent_specs.py b/engine/src/stirling/contracts/agent_specs.py index 403af17807..684872ea8d 100644 --- a/engine/src/stirling/contracts/agent_specs.py +++ b/engine/src/stirling/contracts/agent_specs.py @@ -6,11 +6,11 @@ from pydantic import Field from stirling.models import ApiModel, OperationId -from .common import ToolOperationStep +from .common import StepKind, ToolOperationStep class AiToolAgentStep(ApiModel): - kind: Literal["ai_tool"] = "ai_tool" + kind: Literal[StepKind.AI_TOOL] = StepKind.AI_TOOL title: str description: str tool: OperationId diff --git a/engine/src/stirling/contracts/common.py b/engine/src/stirling/contracts/common.py index 65bb224004..7fd2af9aa5 100644 --- a/engine/src/stirling/contracts/common.py +++ b/engine/src/stirling/contracts/common.py @@ -1,12 +1,89 @@ from __future__ import annotations +from enum import StrEnum from typing import Literal -from pydantic import model_validator +from pydantic import Field, model_validator from stirling.models import OPERATIONS, ApiModel, OperationId, ParamToolModel +class PdfContentType(StrEnum): + """Types of content that can be extracted from a PDF and sent to the AI. + + Java counterpart: AiPdfContentType.java - values must stay in sync. + """ + + # Document-level structured data + PAGE_LAYOUT = "page_layout" + DOCUMENT_METADATA = "document_metadata" + ENCRYPTION_INFO = "encryption_info" + BOOKMARKS = "bookmarks" + LAYERS = "layers" + EMBEDDED_FILES = "embedded_files" + JAVASCRIPT = "javascript" + LINKS = "links" + IMAGE_INFO = "image_info" + FONTS = "fonts" + + # Text and content + PAGE_TEXT = "page_text" + FULL_TEXT = "full_text" + FORM_FIELDS = "form_fields" + ANNOTATIONS = "annotations" + SIGNATURES = "signatures" + STRUCTURE_TREE = "structure_tree" + XMP_METADATA = "xmp_metadata" + + # Heavy content + COMPLIANCE = "compliance" + IMAGES = "images" + + +class WorkflowOutcome(StrEnum): + """Discriminator values for all workflow response unions (outcome field). + + Java counterpart: AiWorkflowOutcome.java - values must stay in sync. + """ + + ANSWER = "answer" + NEED_CONTENT = "need_content" + NOT_FOUND = "not_found" + PLAN = "plan" + NEED_CLARIFICATION = "need_clarification" + CANNOT_DO = "cannot_do" + DRAFT = "draft" + TOOL_CALL = "tool_call" + COMPLETED = "completed" + CANNOT_CONTINUE = "cannot_continue" + UNSUPPORTED_CAPABILITY = "unsupported_capability" + + +class ArtifactKind(StrEnum): + """Discriminator values for WorkflowArtifact unions (kind field). + + Java counterpart: PdfContentExtractor.ArtifactKind - values must stay in sync. + """ + + EXTRACTED_TEXT = "extracted_text" + + +class StepKind(StrEnum): + """Discriminator values for AgentSpecStep unions (kind field).""" + + TOOL = "tool" + AI_TOOL = "ai_tool" + + +class SupportedCapability(StrEnum): + ORCHESTRATE = "orchestrate" + PDF_EDIT = "pdf_edit" + PDF_QUESTION = "pdf_question" + AGENT_DRAFT = "agent_draft" + AGENT_REVISE = "agent_revise" + AGENT_NEXT_ACTION = "agent_next_action" + + class ConversationMessage(ApiModel): role: str content: str @@ -17,8 +94,13 @@ class PdfTextSelection(ApiModel): text: str +class ExtractedFileText(ApiModel): + file_name: str + pages: list[PdfTextSelection] = Field(default_factory=list) + + class ToolOperationStep(ApiModel): - kind: Literal["tool"] = "tool" + kind: Literal[StepKind.TOOL] = StepKind.TOOL tool: OperationId parameters: ParamToolModel diff --git a/engine/src/stirling/contracts/execution.py b/engine/src/stirling/contracts/execution.py index 64e70d682a..6e1f9ad348 100644 --- a/engine/src/stirling/contracts/execution.py +++ b/engine/src/stirling/contracts/execution.py @@ -7,6 +7,7 @@ from pydantic import Field from stirling.models import ApiModel, OperationId, ParamToolModel from .agent_specs import AgentSpec +from .common import WorkflowOutcome class ExecutionStepResult(ApiModel): @@ -31,19 +32,19 @@ class AgentExecutionRequest(ApiModel): class ToolCallExecutionAction(ApiModel): - outcome: Literal["tool_call"] = "tool_call" + outcome: Literal[WorkflowOutcome.TOOL_CALL] = WorkflowOutcome.TOOL_CALL tool: OperationId parameters: ParamToolModel rationale: str | None = None class CompletedExecutionAction(ApiModel): - outcome: Literal["completed"] = "completed" + outcome: Literal[WorkflowOutcome.COMPLETED] = WorkflowOutcome.COMPLETED summary: str class CannotContinueExecutionAction(ApiModel): - outcome: Literal["cannot_continue"] = "cannot_continue" + outcome: Literal[WorkflowOutcome.CANNOT_CONTINUE] = WorkflowOutcome.CANNOT_CONTINUE reason: str diff --git a/engine/src/stirling/contracts/orchestrator.py b/engine/src/stirling/contracts/orchestrator.py index 563341880c..3fa788fe66 100644 --- a/engine/src/stirling/contracts/orchestrator.py +++ b/engine/src/stirling/contracts/orchestrator.py @@ -1,6 +1,5 @@ from __future__ import annotations -from enum import StrEnum from typing import Annotated, Literal from pydantic import Field @@ -8,27 +7,29 @@ from pydantic import Field from stirling.models import ApiModel from .agent_drafts import AgentDraftResponse +from .common import ArtifactKind, ExtractedFileText, SupportedCapability, WorkflowOutcome from .execution import NextExecutionAction from .pdf_edit import PdfEditResponse from .pdf_questions import PdfQuestionResponse -class SupportedCapability(StrEnum): - ORCHESTRATE = "orchestrate" - PDF_EDIT = "pdf_edit" - PDF_QUESTION = "pdf_question" - AGENT_DRAFT = "agent_draft" - AGENT_REVISE = "agent_revise" - AGENT_NEXT_ACTION = "agent_next_action" +class ExtractedTextArtifact(ApiModel): + kind: Literal[ArtifactKind.EXTRACTED_TEXT] = ArtifactKind.EXTRACTED_TEXT + files: list[ExtractedFileText] = Field(default_factory=list) + + +WorkflowArtifact = Annotated[ExtractedTextArtifact, Field(discriminator="kind")] class OrchestratorRequest(ApiModel): user_message: str - conversation_id: str | None = None + file_names: list[str] + artifacts: list[WorkflowArtifact] = Field(default_factory=list) + resume_with: SupportedCapability | None = None class UnsupportedCapabilityResponse(ApiModel): - outcome: Literal["unsupported_capability"] = "unsupported_capability" + outcome: Literal[WorkflowOutcome.UNSUPPORTED_CAPABILITY] = WorkflowOutcome.UNSUPPORTED_CAPABILITY capability: str message: str diff --git a/engine/src/stirling/contracts/pdf_edit.py b/engine/src/stirling/contracts/pdf_edit.py index e3e1e60579..2bcfe7ac6f 100644 --- a/engine/src/stirling/contracts/pdf_edit.py +++ b/engine/src/stirling/contracts/pdf_edit.py @@ -6,30 +6,29 @@ from pydantic import Field from stirling.models import ApiModel -from .common import ToolOperationStep +from .common import ToolOperationStep, WorkflowOutcome class PdfEditRequest(ApiModel): user_message: str - conversation_id: str | None = None file_names: list[str] = Field(default_factory=list) class EditPlanResponse(ApiModel): - outcome: Literal["plan"] = "plan" + outcome: Literal[WorkflowOutcome.PLAN] = WorkflowOutcome.PLAN summary: str rationale: str | None = None steps: list[ToolOperationStep] class EditClarificationRequest(ApiModel): - outcome: Literal["need_clarification"] = "need_clarification" + outcome: Literal[WorkflowOutcome.NEED_CLARIFICATION] = WorkflowOutcome.NEED_CLARIFICATION question: str reason: str class EditCannotDoResponse(ApiModel): - outcome: Literal["cannot_do"] = "cannot_do" + outcome: Literal[WorkflowOutcome.CANNOT_DO] = WorkflowOutcome.CANNOT_DO reason: str diff --git a/engine/src/stirling/contracts/pdf_questions.py b/engine/src/stirling/contracts/pdf_questions.py index 3ac12bdfe8..987dc9b595 100644 --- a/engine/src/stirling/contracts/pdf_questions.py +++ b/engine/src/stirling/contracts/pdf_questions.py @@ -6,31 +6,42 @@ from pydantic import Field from stirling.models import ApiModel +from .common import ExtractedFileText, PdfContentType, SupportedCapability, WorkflowOutcome + class PdfQuestionRequest(ApiModel): question: str - conversation_id: str | None = None - extracted_text: str = "" - file_name: str | None = None + page_text: list[ExtractedFileText] = Field(default_factory=list) + file_names: list[str] class PdfQuestionAnswerResponse(ApiModel): - outcome: Literal["answer"] = "answer" + outcome: Literal[WorkflowOutcome.ANSWER] = WorkflowOutcome.ANSWER answer: str - evidence: list[str] = Field(default_factory=list) + evidence: list[ExtractedFileText] = Field(default_factory=list) -class PdfQuestionNeedTextResponse(ApiModel): - outcome: Literal["need_text"] = "need_text" +class NeedContentFileRequest(ApiModel): + file_name: str + page_numbers: list[int] = Field(default_factory=list) + content_types: list[PdfContentType] + + +class PdfQuestionNeedContentResponse(ApiModel): + outcome: Literal[WorkflowOutcome.NEED_CONTENT] = WorkflowOutcome.NEED_CONTENT + resume_with: SupportedCapability = SupportedCapability.PDF_QUESTION reason: str + files: list[NeedContentFileRequest] = Field(default_factory=list) + max_pages: int + max_characters: int class PdfQuestionNotFoundResponse(ApiModel): - outcome: Literal["not_found"] = "not_found" + outcome: Literal[WorkflowOutcome.NOT_FOUND] = WorkflowOutcome.NOT_FOUND reason: str PdfQuestionResponse = Annotated[ - PdfQuestionAnswerResponse | PdfQuestionNeedTextResponse | PdfQuestionNotFoundResponse, + PdfQuestionAnswerResponse | PdfQuestionNeedContentResponse | PdfQuestionNotFoundResponse, Field(discriminator="outcome"), ] diff --git a/engine/tests/test_pdf_question_agent.py b/engine/tests/test_pdf_question_agent.py index 52df9474ce..a4a1ce7bfd 100644 --- a/engine/tests/test_pdf_question_agent.py +++ b/engine/tests/test_pdf_question_agent.py @@ -5,10 +5,12 @@ import pytest from stirling.agents import PdfQuestionAgent from stirling.config import AppSettings from stirling.contracts import ( + ExtractedFileText, PdfQuestionAnswerResponse, - PdfQuestionNeedTextResponse, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, + PdfTextSelection, ) from stirling.services import build_runtime @@ -34,13 +36,22 @@ def build_test_settings() -> AppSettings: ) +def invoice_page() -> ExtractedFileText: + return ExtractedFileText( + file_name="invoice.pdf", + pages=[PdfTextSelection(page_number=1, text="Invoice total: 120.00")], + ) + + @pytest.mark.anyio async def test_pdf_question_agent_requires_extracted_text() -> None: agent = PdfQuestionAgent(build_runtime(build_test_settings())) - response = await agent.handle(PdfQuestionRequest(question="What is the total?", extracted_text="")) + response = await agent.handle( + PdfQuestionRequest(question="What is the total?", page_text=[], file_names=["test.pdf"]) + ) - assert isinstance(response, PdfQuestionNeedTextResponse) + assert isinstance(response, PdfQuestionNeedContentResponse) @pytest.mark.anyio @@ -48,15 +59,15 @@ async def test_pdf_question_agent_returns_grounded_answer() -> None: agent = StubPdfQuestionAgent( PdfQuestionAnswerResponse( answer="The invoice total is 120.00.", - evidence=["Invoice total: 120.00"], + evidence=[invoice_page()], ) ) response = await agent.handle( PdfQuestionRequest( question="What is the total?", - extracted_text="Invoice total: 120.00", - file_name="invoice.pdf", + page_text=[invoice_page()], + file_names=["invoice.pdf"], ) ) @@ -71,8 +82,13 @@ async def test_pdf_question_agent_returns_not_found_when_text_is_insufficient() response = await agent.handle( PdfQuestionRequest( question="What is the total?", - extracted_text="This page contains only a shipping address.", - file_name="invoice.pdf", + page_text=[ + ExtractedFileText( + file_name="invoice.pdf", + pages=[PdfTextSelection(page_number=1, text="This page contains only a shipping address.")], + ) + ], + file_names=["invoice.pdf"], ) ) diff --git a/engine/tests/test_stirling_api.py b/engine/tests/test_stirling_api.py index 9191eeb441..774fb28d9d 100644 --- a/engine/tests/test_stirling_api.py +++ b/engine/tests/test_stirling_api.py @@ -20,9 +20,9 @@ from stirling.contracts import ( EditCannotDoResponse, OrchestratorRequest, PdfEditRequest, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, - UnsupportedCapabilityResponse, ) from stirling.models.tool_models import RotateParams @@ -38,8 +38,8 @@ class StubSettingsProvider: class StubOrchestratorAgent: - async def handle(self, request: OrchestratorRequest) -> UnsupportedCapabilityResponse: - return UnsupportedCapabilityResponse(capability="pdf_edit", message=request.user_message) + async def handle(self, request: OrchestratorRequest) -> PdfQuestionNeedContentResponse: + return PdfQuestionNeedContentResponse(reason=request.user_message, files=[], max_pages=1, max_characters=1000) class StubPdfEditAgent: @@ -115,10 +115,10 @@ def test_health_route() -> None: def test_orchestrator_route() -> None: - response = client.post("/api/v1/orchestrator", json={"userMessage": "route this"}) + response = client.post("/api/v1/orchestrator", json={"userMessage": "route this", "fileNames": ["test.pdf"]}) assert response.status_code == 200 - assert response.json()["outcome"] == "unsupported_capability" + assert response.json()["outcome"] == "need_content" def test_pdf_edit_route() -> None: @@ -129,7 +129,14 @@ def test_pdf_edit_route() -> None: def test_pdf_questions_route() -> None: - response = client.post("/api/v1/pdf/questions", json={"question": "what is this?"}) + response = client.post( + "/api/v1/pdf/questions", + json={ + "question": "what is this?", + "fileNames": ["test.pdf"], + "pageText": [{"fileName": "test.pdf", "pages": [{"pageNumber": 1, "text": "Example"}]}], + }, + ) assert response.status_code == 200 assert response.json()["outcome"] == "not_found" diff --git a/engine/tests/test_stirling_contracts.py b/engine/tests/test_stirling_contracts.py index 7cf7df974c..63e283a641 100644 --- a/engine/tests/test_stirling_contracts.py +++ b/engine/tests/test_stirling_contracts.py @@ -9,17 +9,34 @@ from stirling.contracts import ( AgentSpecStep, EditPlanResponse, ExecutionContext, + ExtractedFileText, + ExtractedTextArtifact, OrchestratorRequest, PdfQuestionAnswerResponse, + PdfTextSelection, ToolOperationStep, ) from stirling.models.tool_models import OperationId, RotateParams def test_orchestrator_request_accepts_user_message() -> None: - request = OrchestratorRequest(user_message="Rotate the PDF") + request = OrchestratorRequest( + user_message="Rotate the PDF", + file_names=["test.pdf"], + artifacts=[ + ExtractedTextArtifact( + files=[ + ExtractedFileText( + file_name="test.pdf", + pages=[PdfTextSelection(page_number=1, text="Hello")], + ) + ] + ) + ], + ) assert request.user_message == "Rotate the PDF" + assert len(request.artifacts) == 1 def test_agent_execution_request_uses_typed_agent_spec() -> None: From 74e62b86a28b945d0a6c4c882b563f265401aed2 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 9 Apr 2026 09:21:07 +0100 Subject: [PATCH 24/59] Add prototypes folder to test new functionality in (#6081) # Description of Changes Add prototypes folder to test new functionality in. This build of the app is spawnable with `npm run dev:prototypes`. Currently just contains a very developer-y chat interface to help us develop & explore the AI backend before we make the frontend for it for real. --- LICENSE | 2 + frontend/eslint.config.mjs | 1 + frontend/package.json | 5 +- frontend/src/prototypes/App.tsx | 81 ++++++++ frontend/src/prototypes/LICENSE | 51 +++++ .../prototypes/components/AppProviders.tsx | 18 ++ .../components/chat/ChatContext.tsx | 180 ++++++++++++++++++ .../prototypes/components/chat/ChatPanel.css | 74 +++++++ .../prototypes/components/chat/ChatPanel.tsx | 135 +++++++++++++ .../components/home/HomePageExtensions.tsx | 5 + frontend/src/prototypes/tsconfig.json | 27 +++ frontend/tsconfig.prototypes.vite.json | 23 +++ frontend/vite.config.ts | 3 +- frontend/vitest.config.ts | 18 ++ 14 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 frontend/src/prototypes/App.tsx create mode 100644 frontend/src/prototypes/LICENSE create mode 100644 frontend/src/prototypes/components/AppProviders.tsx create mode 100644 frontend/src/prototypes/components/chat/ChatContext.tsx create mode 100644 frontend/src/prototypes/components/chat/ChatPanel.css create mode 100644 frontend/src/prototypes/components/chat/ChatPanel.tsx create mode 100644 frontend/src/prototypes/components/home/HomePageExtensions.tsx create mode 100644 frontend/src/prototypes/tsconfig.json create mode 100644 frontend/tsconfig.prototypes.vite.json diff --git a/LICENSE b/LICENSE index e7a8034e41..ea74278a4e 100644 --- a/LICENSE +++ b/LICENSE @@ -14,6 +14,8 @@ if that directory exists, is licensed under the license defined in "frontend/src if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE". * All content that resides under the "frontend/src/saas/" directory of this repository, if that directory exists, is licensed under the license defined in "frontend/src/saas/LICENSE". +* All content that resides under the "frontend/src/prototypes/" directory of this repository, +if that directory exists, is licensed under the license defined in "frontend/src/prototypes/LICENSE". * Content outside of the above mentioned directories or restrictions above is available under the MIT License as defined below. diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index efe45f17b5..9bbe913d60 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -86,6 +86,7 @@ export default defineConfig( files: [ 'src/proprietary/**/*.{js,mjs,jsx,ts,tsx}', 'src/saas/**/*.{js,mjs,jsx,ts,tsx}', + 'src/prototypes/**/*.{js,mjs,jsx,ts,tsx}', ], languageOptions: { parserOptions: { diff --git a/frontend/package.json b/frontend/package.json index b4c1f6263c..17c031f7a5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -88,6 +88,7 @@ "dev:proprietary": "npm run prep && vite --mode proprietary", "dev:saas": "npm run prep:saas && vite --mode saas", "dev:desktop": "npm run prep:desktop && vite --mode desktop", + "dev:prototypes": "npm run prep && vite --mode prototypes", "lint": "npm run lint:eslint && npm run lint:cycles", "lint:eslint": "eslint --max-warnings=0", "lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1", @@ -96,6 +97,7 @@ "build:proprietary": "npm run prep && vite build --mode proprietary", "build:saas": "npm run prep:saas && vite build --mode saas", "build:desktop": "npm run prep:desktop && vite build --mode desktop", + "build:prototypes": "npm run prep && vite build --mode prototypes", "preview": "vite preview", "tauri-dev": "npm run prep:desktop && tauri dev --no-watch", "tauri-build": "npm run prep:desktop-build && tauri build", @@ -110,8 +112,9 @@ "typecheck:proprietary": "tsc --noEmit --project src/proprietary/tsconfig.json", "typecheck:saas": "tsc --noEmit --project src/saas/tsconfig.json", "typecheck:desktop": "tsc --noEmit --project src/desktop/tsconfig.json", + "typecheck:prototypes": "tsc --noEmit --project src/prototypes/tsconfig.json", "typecheck:scripts": "tsc --noEmit --project scripts/tsconfig.json", - "typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:scripts", + "typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:prototypes && npm run typecheck:scripts", "check": "npm run typecheck && npm run lint && npm run test:run", "generate-licenses": "node scripts/generate-licenses.js", "generate-icons": "node scripts/generate-icons.js", diff --git a/frontend/src/prototypes/App.tsx b/frontend/src/prototypes/App.tsx new file mode 100644 index 0000000000..2b3b67b19b --- /dev/null +++ b/frontend/src/prototypes/App.tsx @@ -0,0 +1,81 @@ +import { Suspense } from "react"; +import { Routes, Route, useParams } from "react-router-dom"; +import { AppProviders } from "@app/components/AppProviders"; +import { AppLayout } from "@app/components/AppLayout"; +import { LoadingFallback } from "@app/components/shared/LoadingFallback"; +import { PreferencesProvider } from "@app/contexts/PreferencesContext"; +import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider"; +import Landing from "@app/routes/Landing"; +import Login from "@app/routes/Login"; +import Signup from "@app/routes/Signup"; +import AuthCallback from "@app/routes/AuthCallback"; +import InviteAccept from "@app/routes/InviteAccept"; +import ShareLinkPage from "@app/routes/ShareLinkPage"; +import ParticipantView from "@app/components/workflow/ParticipantView"; +import Onboarding from "@app/components/onboarding/Onboarding"; + +// Import global styles +import "@app/styles/tailwind.css"; +import "@app/styles/cookieconsent.css"; +import "@app/styles/index.css"; +import "@app/styles/auth-theme.css"; + +// Import file ID debugging helpers (development only) +import "@app/utils/fileIdSafety"; + +// Minimal providers for public routes - no API calls, no authentication +function MinimalProviders({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + +// Participant signing page — token-gated, no login required +function ParticipantViewPage() { + const { token } = useParams<{ token: string }>(); + if (!token) return null; + return ; +} + +export default function App() { + return ( + }> + + {/* Participant signing — public, token-gated, no auth required */} + + + + } + /> + + {/* All other routes need AppProviders for backend integration */} + + + + } /> + } /> + } /> + } /> + } /> + {/* Main app routes - Landing handles auth logic */} + } /> + + + + + } + /> + + + ); +} diff --git a/frontend/src/prototypes/LICENSE b/frontend/src/prototypes/LICENSE new file mode 100644 index 0000000000..d268556808 --- /dev/null +++ b/frontend/src/prototypes/LICENSE @@ -0,0 +1,51 @@ +Stirling PDF User License + +Copyright (c) 2025 Stirling PDF Inc. + +License Scope & Usage Rights + +Production use of the Stirling PDF Software is only permitted with a valid Stirling PDF User License. + +For purposes of this license, “the Software” refers to the Stirling PDF application and any associated documentation files +provided by Stirling PDF Inc. You or your organization may not use the Software in production, at scale, or for business-critical +processes unless you have agreed to, and remain in compliance with, the Stirling PDF Subscription Terms of Service +(https://www.stirlingpdf.com/terms) or another valid agreement with Stirling PDF, and hold an active User License subscription +covering the appropriate number of licensed users. + +Trial and Minimal Use + +You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that: +* Use is limited to the capabilities and restrictions defined by the Software itself; +* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts. + +Continued use beyond this scope requires a valid Stirling PDF User License. + +Modifications and Derivative Works + +You may modify the Software only for development or internal testing purposes. Any such modifications or derivative works: + +* May not be deployed in production environments without a valid User License; +* May not be distributed or sublicensed; +* Remain the intellectual property of Stirling PDF and/or its licensors; +* May only be used, copied, or exploited in accordance with the terms of a valid Stirling PDF User License subscription. + +Prohibited Actions + +Unless explicitly permitted by a paid license or separate agreement, you may not: + +* Use the Software in production environments; +* Copy, merge, distribute, sublicense, or sell the Software; +* Remove or alter any licensing or copyright notices; +* Circumvent access restrictions or licensing requirements. + +Third-Party Components + +The Stirling PDF Software may include components subject to separate open source licenses. Such components remain governed by +their original license terms as provided by their respective owners. + +Disclaimer + +THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/frontend/src/prototypes/components/AppProviders.tsx b/frontend/src/prototypes/components/AppProviders.tsx new file mode 100644 index 0000000000..f8729b1830 --- /dev/null +++ b/frontend/src/prototypes/components/AppProviders.tsx @@ -0,0 +1,18 @@ +import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders"; +import { type AppProvidersProps } from "@core/components/AppProviders"; +import { ChatProvider } from "@app/components/chat/ChatContext"; + +export type { AppProvidersProps }; + +export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/prototypes/components/chat/ChatContext.tsx b/frontend/src/prototypes/components/chat/ChatContext.tsx new file mode 100644 index 0000000000..b92f17280e --- /dev/null +++ b/frontend/src/prototypes/components/chat/ChatContext.tsx @@ -0,0 +1,180 @@ +import { createContext, useContext, useReducer, useCallback, type ReactNode } from "react"; +import { useAllFiles } from "@app/contexts/FileContext"; + +export interface ChatMessage { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: number; +} + +type AiWorkflowOutcome = + | "answer" + | "not_found" + | "need_content" + | "plan" + | "need_clarification" + | "cannot_do" + | "tool_call" + | "completed" + | "unsupported_capability" + | "cannot_continue"; + +interface AiWorkflowResponse { + outcome: AiWorkflowOutcome; + answer?: string; + summary?: string; + rationale?: string; + reason?: string; + question?: string; + capability?: string; + message?: string; + evidence?: Array<{ pageNumber: number; text: string }>; + steps?: Array>; +} + +interface ChatState { + messages: ChatMessage[]; + isOpen: boolean; + isLoading: boolean; +} + +type ChatAction = + | { type: "ADD_MESSAGE"; message: ChatMessage } + | { type: "SET_LOADING"; loading: boolean } + | { type: "TOGGLE_OPEN" } + | { type: "SET_OPEN"; open: boolean }; + +function chatReducer(state: ChatState, action: ChatAction): ChatState { + switch (action.type) { + case "ADD_MESSAGE": + return { ...state, messages: [...state.messages, action.message] }; + case "SET_LOADING": + return { ...state, isLoading: action.loading }; + case "TOGGLE_OPEN": + return { ...state, isOpen: !state.isOpen }; + case "SET_OPEN": + return { ...state, isOpen: action.open }; + } +} + +function formatWorkflowResponse(data: AiWorkflowResponse): string { + switch (data.outcome) { + case "answer": + case "completed": + return data.answer ?? data.summary ?? "Done."; + case "need_clarification": + return data.question ?? "Could you clarify your request?"; + case "cannot_do": + return data.reason ?? "I'm unable to do that."; + case "not_found": + return data.reason ?? "I couldn't find the requested information."; + case "unsupported_capability": + return data.message ?? `Unsupported capability: ${data.capability ?? "unknown"}`; + case "cannot_continue": + return data.reason ?? "Something went wrong and I can't continue."; + case "plan": + return data.rationale + ? `${data.rationale}\n\n${(data.steps ?? []).map((s, i) => `${i + 1}. ${JSON.stringify(s)}`).join("\n")}` + : JSON.stringify(data.steps, null, 2); + case "need_content": + case "tool_call": + return data.rationale ?? data.summary ?? `Processing (${data.outcome})...`; + default: + return data.answer ?? data.summary ?? data.message ?? JSON.stringify(data); + } +} + +interface ChatContextValue { + messages: ChatMessage[]; + isOpen: boolean; + isLoading: boolean; + toggleOpen: () => void; + setOpen: (open: boolean) => void; + sendMessage: (content: string) => Promise; +} + +const ChatContext = createContext(null); + +const initialState: ChatState = { + messages: [], + isOpen: false, + isLoading: false, +}; + +export function ChatProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(chatReducer, initialState); + const { files: activeFiles } = useAllFiles(); + + const toggleOpen = useCallback(() => dispatch({ type: "TOGGLE_OPEN" }), []); + const setOpen = useCallback((open: boolean) => dispatch({ type: "SET_OPEN", open }), []); + + const sendMessage = useCallback(async (content: string) => { + const userMessage: ChatMessage = { + id: crypto.randomUUID(), + role: "user", + content, + timestamp: Date.now(), + }; + dispatch({ type: "ADD_MESSAGE", message: userMessage }); + dispatch({ type: "SET_LOADING", loading: true }); + + try { + const formData = new FormData(); + formData.append("userMessage", content); + activeFiles.forEach((file, i) => { + formData.append(`fileInputs[${i}].fileInput`, file); + }); + + const response = await fetch("/api/v1/ai/orchestrate", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + throw new Error(`AI engine request failed: ${response.status}`); + } + + const data: AiWorkflowResponse = await response.json(); + const replyContent = formatWorkflowResponse(data); + const assistantMessage: ChatMessage = { + id: crypto.randomUUID(), + role: "assistant", + content: replyContent, + timestamp: Date.now(), + }; + dispatch({ type: "ADD_MESSAGE", message: assistantMessage }); + } catch { + const errorMessage: ChatMessage = { + id: crypto.randomUUID(), + role: "assistant", + content: "Failed to get a response. The AI engine may not be available yet.", + timestamp: Date.now(), + }; + dispatch({ type: "ADD_MESSAGE", message: errorMessage }); + } finally { + dispatch({ type: "SET_LOADING", loading: false }); + } + }, [activeFiles]); + + return ( + + {children} + + ); +} + +export function useChat(): ChatContextValue { + const context = useContext(ChatContext); + if (!context) { + throw new Error("useChat must be used within a ChatProvider"); + } + return context; +} diff --git a/frontend/src/prototypes/components/chat/ChatPanel.css b/frontend/src/prototypes/components/chat/ChatPanel.css new file mode 100644 index 0000000000..8f153b925b --- /dev/null +++ b/frontend/src/prototypes/components/chat/ChatPanel.css @@ -0,0 +1,74 @@ +.chat-toggle-button { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.chat-panel { + position: fixed; + top: 0; + right: 0; + width: 380px; + height: 100vh; + display: flex; + flex-direction: column; + background: var(--mantine-color-body); + border-left: 1px solid var(--border-subtle, var(--mantine-color-default-border)); + z-index: 999; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); +} + +.chat-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-subtle, var(--mantine-color-default-border)); + flex-shrink: 0; +} + +.chat-panel-messages { + flex: 1; + min-height: 0; +} + +.chat-panel-input { + display: flex; + gap: 0.5rem; + padding: 0.75rem; + border-top: 1px solid var(--border-subtle, var(--mantine-color-default-border)); + flex-shrink: 0; +} + +/* Message layout */ +.chat-message { + display: flex; +} + +.chat-message-user { + justify-content: flex-end; +} + +.chat-message-assistant { + justify-content: flex-start; +} + +/* Bubble styling */ +.chat-bubble { + max-width: 85%; +} + +.chat-bubble-user { + background: var(--mantine-color-blue-filled) !important; + color: white !important; +} + +.chat-bubble-user * { + color: white !important; +} + +.chat-bubble-assistant { + background: var(--mantine-color-default-hover) !important; +} diff --git a/frontend/src/prototypes/components/chat/ChatPanel.tsx b/frontend/src/prototypes/components/chat/ChatPanel.tsx new file mode 100644 index 0000000000..f47a185de7 --- /dev/null +++ b/frontend/src/prototypes/components/chat/ChatPanel.tsx @@ -0,0 +1,135 @@ +import { useRef, useEffect, useState, type KeyboardEvent } from "react"; +import { ActionIcon, ScrollArea, TextInput, Stack, Text, Paper, Box, Transition } from "@mantine/core"; +import SendIcon from "@mui/icons-material/Send"; +import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; +import CloseIcon from "@mui/icons-material/Close"; +import { useChat } from "@app/components/chat/ChatContext"; +import "@app/components/chat/ChatPanel.css"; + +function ChatMessageBubble({ role, content }: { role: "user" | "assistant"; content: string }) { + return ( +

+ + {content} + +
+ ); +} + +export function ChatPanel() { + const { messages, isOpen, isLoading, toggleOpen, sendMessage } = useChat(); + const [input, setInput] = useState(""); + const scrollRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); + } + }, [messages]); + + useEffect(() => { + if (isOpen) { + inputRef.current?.focus(); + } + }, [isOpen]); + + const handleSend = () => { + const trimmed = input.trim(); + if (!trimmed || isLoading) return; + setInput(""); + sendMessage(trimmed); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( + <> + {/* Toggle button - always visible */} + {!isOpen && ( + + + + )} + + {/* Chat panel */} + + {(styles) => ( + + {/* Header */} +
+ AI Assistant + + + +
+ + {/* Messages */} + + + {messages.length === 0 && ( + + Ask a question about your documents or get help with PDF tools. + + )} + {messages.map((msg) => ( + + ))} + {isLoading && ( +
+ + Thinking... + +
+ )} +
+
+ + {/* Input */} +
+ setInput(e.currentTarget.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + rightSection={ + + + + } + rightSectionWidth={36} + style={{ flex: 1 }} + /> +
+
+ )} +
+ + ); +} diff --git a/frontend/src/prototypes/components/home/HomePageExtensions.tsx b/frontend/src/prototypes/components/home/HomePageExtensions.tsx new file mode 100644 index 0000000000..1e44646ce2 --- /dev/null +++ b/frontend/src/prototypes/components/home/HomePageExtensions.tsx @@ -0,0 +1,5 @@ +import { ChatPanel } from "@app/components/chat/ChatPanel"; + +export function HomePageExtensions() { + return ; +} diff --git a/frontend/src/prototypes/tsconfig.json b/frontend/src/prototypes/tsconfig.json new file mode 100644 index 0000000000..5dad506bd5 --- /dev/null +++ b/frontend/src/prototypes/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "../../", + "paths": { + "@app/*": [ + "src/prototypes/*", + "src/proprietary/*", + "src/core/*" + ], + "@proprietary/*": [ + "src/proprietary/*" + ], + "@core/*": [ + "src/core/*" + ] + } + }, + "include": [ + "../global.d.ts", + "../*.js", + "../*.ts", + "../*.tsx", + "../core/setupTests.ts", + "." + ] +} diff --git a/frontend/tsconfig.prototypes.vite.json b/frontend/tsconfig.prototypes.vite.json new file mode 100644 index 0000000000..6d5e4629b9 --- /dev/null +++ b/frontend/tsconfig.prototypes.vite.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.proprietary.vite.json", + "compilerOptions": { + "paths": { + "@app/*": [ + "src/prototypes/*", + "src/proprietary/*", + "src/core/*" + ], + "@proprietary/*": ["src/proprietary/*"], + "@core/*": ["src/core/*"] + } + }, + "exclude": [ + "src/core/**/*.test.ts*", + "src/core/**/*.spec.ts*", + "src/proprietary/**/*.test.ts*", + "src/proprietary/**/*.spec.ts*", + "src/desktop", + "src/saas", + "node_modules" + ] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4e36220aaf..1a5a85f413 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react-swc'; import tsconfigPaths from 'vite-tsconfig-paths'; import { viteStaticCopy } from 'vite-plugin-static-copy'; -const VALID_MODES = ['core', 'proprietary', 'saas', 'desktop'] as const; +const VALID_MODES = ['core', 'proprietary', 'saas', 'desktop', 'prototypes'] as const; type BuildMode = typeof VALID_MODES[number]; const TSCONFIG_MAP: Record = { @@ -11,6 +11,7 @@ const TSCONFIG_MAP: Record = { proprietary: './tsconfig.proprietary.vite.json', saas: './tsconfig.saas.vite.json', desktop: './tsconfig.desktop.vite.json', + prototypes: './tsconfig.prototypes.vite.json', }; export default defineConfig(({ mode }) => { diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 63fef12cfb..dc029485aa 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -100,6 +100,24 @@ export default defineConfig({ target: 'es2020' } }, + { + test: { + name: 'prototypes', + include: ['src/prototypes/**/*.test.{ts,tsx}'], + environment: 'jsdom', + globals: true, + setupFiles: ['./src/core/setupTests.ts'], + }, + plugins: [ + react(), + tsconfigPaths({ + projects: ['./tsconfig.prototypes.vite.json'], + }), + ], + esbuild: { + target: 'es2020' + } + }, ], }, esbuild: { From d5b7af6567b38899f2996b7ec45debaa16b91eb0 Mon Sep 17 00:00:00 2001 From: Vibe Stack Date: Thu, 9 Apr 2026 06:30:19 -0400 Subject: [PATCH 25/59] feat(settings): add default startup view and reader zoom preferences (#6073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description of Changes Adds two new user preferences to the General settings panel, addressing #5908. **Default view on launch** - a segmented control (Tools / Reader / Automate) that controls which left-column tab is active when the app starts. Previously the app always opened on the Tools tab with no way to change this. Users who spend most of their time reading PDFs had to manually switch to the Reader tab on every launch. **Default reader zoom** - a dropdown (Auto / Fit width / Fit page / 50%–200%) that sets the initial zoom level whenever a PDF is opened in the reader. Previously the app always applied an automatic fit-to-viewport calculation. Both settings are non-breaking. The defaults (`Tools` and `Auto`) reproduce the existing behaviour exactly, so existing users see no difference until they change a preference. ### What changed - `preferencesService.ts` - added `StartupView` and `ViewerZoomSetting` types plus the two new fields to `UserPreferences` with safe defaults - `ToolWorkflowContext.tsx` - one-time startup effect that navigates to the preferred tab on first render (mirrors the existing `defaultToolPanelMode` sync pattern) - `ZoomAPIBridge.tsx` - respects the zoom preference before falling back to auto-zoom logic when a document loads - `GeneralSection.tsx` - two new controls added below "Default tool picker mode"; the Select uses `comboboxProps={{ withinPortal: true }}` so the dropdown renders above the settings modal - `en-GB/translation.toml` - new keys for labels, descriptions, and option values Closes #5908 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [x] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [x] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) Screenshot 2026-04-05 185718 Screenshot 2026-04-05 185620 ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.toml | 14 +++++ .../config/configSections/GeneralSection.tsx | 58 +++++++++++++++++++ .../core/components/viewer/ZoomAPIBridge.tsx | 16 +++++ .../src/core/contexts/ToolWorkflowContext.tsx | 22 +++++++ .../src/core/services/preferencesService.ts | 8 +++ 5 files changed, 118 insertions(+) diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 5a8565d5a5..a91985ee41 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -6638,9 +6638,13 @@ defaultPdfEditorActive = "Stirling PDF is your default PDF editor" defaultPdfEditorChecking = "Checking..." defaultPdfEditorInactive = "Another application is set as default" defaultPdfEditorSet = "Already Default" +defaultStartupView = "Default view on launch" +defaultStartupViewDescription = "Choose which tab is active in the left column when the app starts" defaultToolPickerMode = "Default tool picker mode" defaultToolPickerModeDescription = "Choose whether the tool picker opens in fullscreen or sidebar by default" description = "Configure general application preferences." +defaultViewerZoom = "Default reader zoom" +defaultViewerZoomDescription = "Set the default zoom level when opening PDFs in the reader" hideUnavailableConversions = "Hide unavailable conversions" hideUnavailableConversionsDescription = "Remove disabled conversion options in the Convert tool instead of showing them greyed out." hideUnavailableTools = "Hide unavailable tools" @@ -6663,6 +6667,16 @@ title = "For System Administrators" fullscreen = "Fullscreen" sidebar = "Sidebar" +[settings.general.startupView] +automate = "Automate" +read = "Reader" +tools = "Tools" + +[settings.general.zoomLevel] +auto = "Auto" +fitPage = "Fit page" +fitWidth = "Fit width" + [settings.general.updates] checkForUpdates = "Check for Updates" currentBackendVersion = "Current Backend Version" diff --git a/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx b/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx index 2d4c9f42de..d65bdcc8bf 100644 --- a/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx @@ -7,6 +7,7 @@ import { Tooltip, NumberInput, SegmentedControl, + Select, Code, Group, Anchor, @@ -19,6 +20,8 @@ import { useTranslation } from "react-i18next"; import { usePreferences } from "@app/contexts/PreferencesContext"; import { useAppConfig } from "@app/contexts/AppConfigContext"; import type { ToolPanelMode } from "@app/constants/toolPanel"; +import type { StartupView, ViewerZoomSetting } from "@app/services/preferencesService"; +import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; import LocalIcon from "@app/components/shared/LocalIcon"; import { updateService, UpdateSummary } from "@app/services/updateService"; import UpdateModal from "@app/components/shared/UpdateModal"; @@ -293,6 +296,61 @@ const GeneralSection: React.FC = ({ hideTitle = false, hide ]} /> +
+
+ + {t("settings.general.defaultStartupView", "Default view on launch")} + + + {t( + "settings.general.defaultStartupViewDescription", + "Choose which tab is active in the left column when the app starts", + )} + +
+ updatePreference("defaultStartupView", val as StartupView)} + data={[ + { label: t("settings.general.startupView.tools", "Tools"), value: "tools" }, + { label: t("settings.general.startupView.read", "Reader"), value: "read" }, + { label: t("settings.general.startupView.automate", "Automate"), value: "automate" }, + ]} + /> +
+
+
+ + {t("settings.general.defaultViewerZoom", "Default reader zoom")} + + + {t( + "settings.general.defaultViewerZoomDescription", + "Set the default zoom level when opening PDFs in the reader", + )} + +
+ + + ); +} diff --git a/frontend/src/core/components/shared/LandingDocumentStack.tsx b/frontend/src/core/components/shared/LandingDocumentStack.tsx new file mode 100644 index 0000000000..3fffd48602 --- /dev/null +++ b/frontend/src/core/components/shared/LandingDocumentStack.tsx @@ -0,0 +1,47 @@ +/** Decorative stack only: window dots + grey bars — no text or i18n (avoids keys showing in the UI). */ +export function LandingDocumentStack() { + const bar = (widthPct: number, heightPx: number, marginBottom: number) => ({ + width: `${widthPct}%`, + height: heightPx, + marginBottom: marginBottom || undefined, + }); + + return ( +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/core/components/shared/LandingPage.css b/frontend/src/core/components/shared/LandingPage.css new file mode 100644 index 0000000000..60c2f3730e --- /dev/null +++ b/frontend/src/core/components/shared/LandingPage.css @@ -0,0 +1,137 @@ +/* ============================================================ + Landing Page styles. + All custom properties are defined in theme.css. + ============================================================ */ + +/* ── Hero text ───────────────────────────────────────────── */ +.landing-title { + margin: 0; + margin-top: 1.75rem; + margin-bottom: 0.5rem; + text-align: center; + font-size: 2.125rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-primary); +} + +.landing-subtitle { + margin: 0; + margin-bottom: 1.5rem; + text-align: center; + font-size: 0.9375rem; + line-height: 1.5; + max-width: 28rem; + color: var(--text-secondary); +} + +/* ── Document stack ──────────────────────────────────────── */ +.landing-stack { + position: relative; + z-index: 1; + width: var(--landing-stack-w); + min-width: var(--landing-stack-w); + height: var(--landing-stack-h); + min-height: var(--landing-stack-h); + margin-left: auto; + margin-right: auto; + flex-shrink: 0; + overflow: visible; +} + +/* Sheets — static white, never change with theme */ +.landing-sheet { + position: absolute; + border-radius: 12px; + background-color: #ffffff; + cursor: default; +} + +.landing-sheet--back { + width: 128px; + height: 160px; + transform-origin: bottom center; + border: 1px solid #e5e7eb; + box-shadow: var(--landing-doc-shadow-back-idle); +} + +.landing-sheet--left { + left: 8px; + top: 12px; + transform: rotate(-8deg); +} + +.landing-sheet--right { + right: 8px; + top: 12px; + transform: rotate(8deg); +} + +.landing-sheet--front { + left: 50%; + top: 0; + z-index: 10; + width: 144px; + height: 176px; + margin-left: -72px; + overflow: hidden; + box-shadow: var(--landing-doc-shadow-front-idle); +} + +.landing-sheet-header { + display: flex; + height: 40px; + align-items: center; + gap: 8px; + padding: 0 12px; + border-radius: 12px 12px 0 0; + background: var(--landing-hero-gradient); +} + +.landing-sheet-dot { + width: 10px; + height: 10px; + border-radius: 9999px; +} + +.landing-sheet-body { + padding: 10px 12px; +} + +.landing-sheet-side-body { + padding: 12px; +} + +/* Bars — static light colours, never change with theme */ +.landing-bar { + border-radius: 9999px; + background-color: #e5e7eb; +} +.landing-bar--strong { + background-color: #d1d5db; +} + +/* ── Action buttons ──────────────────────────────────────── */ +.landing-btn-primary { + background: var(--landing-hero-gradient) !important; + color: #ffffff !important; + border: none !important; + border-radius: 0.75rem !important; + font-weight: 600 !important; +} + +.landing-btn-secondary { + border-radius: 0.75rem !important; + font-weight: 600 !important; + border-color: var(--landing-button-border, var(--border-default)) !important; + background-color: var(--landing-button-bg, var(--bg-surface)) !important; + color: var(--landing-button-color, var(--text-primary)) !important; +} +.landing-btn-secondary:hover { + background-color: var(--landing-button-hover-bg, var(--landing-button-bg, var(--bg-surface))) !important; +} + +/* Icon-only variant: accent colour instead of button text colour */ +.landing-btn-icon { + color: var(--accent-interactive) !important; +} diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index a2ad72e2a4..1d9b878f46 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -1,51 +1,30 @@ -import React, { useEffect } from 'react'; -import { Container, Button, Group, useMantineColorScheme, ActionIcon, Tooltip } from '@mantine/core'; +import React, { useState } from 'react'; +import { Container } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import LocalIcon from '@app/components/shared/LocalIcon'; import { useTranslation } from 'react-i18next'; import { useFileHandler } from '@app/hooks/useFileHandler'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useLogoPath } from '@app/hooks/useLogoPath'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import { useLogoVariant } from '@app/hooks/useLogoVariant'; -import { useFileManager } from '@app/hooks/useFileManager'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { useIsMobile } from '@app/hooks/useIsMobile'; import MobileUploadModal from '@app/components/shared/MobileUploadModal'; import { openFilesFromDisk } from '@app/services/openFilesFromDisk'; +import { LandingDocumentStack } from '@app/components/shared/LandingDocumentStack'; +import { LandingActions } from '@app/components/shared/LandingActions'; +import '@app/components/shared/LandingPage.css'; const LandingPage = () => { - const { addFiles } = useFileHandler(); - const fileInputRef = React.useRef(null); - const { colorScheme } = useMantineColorScheme(); const { t } = useTranslation(); - const { openFilesModal } = useFilesModalContext(); - const [isUploadHover, setIsUploadHover] = React.useState(false); - const logoPath = useLogoPath(); - const logoVariant = useLogoVariant(); - const { wordmark } = useLogoAssets(); - const { loadRecentFiles } = useFileManager(); - const [hasRecents, setHasRecents] = React.useState(false); - const [mobileUploadModalOpen, setMobileUploadModalOpen] = React.useState(false); + const { addFiles } = useFileHandler(); + const fileInputRef = React.useRef(null); const terminology = useFileActionTerminology(); - const icons = useFileActionIcons(); - const { config } = useAppConfig(); - const isMobile = useIsMobile(); + const [mobileUploadModalOpen, setMobileUploadModalOpen] = useState(false); const handleFileDrop = async (files: File[]) => { await addFiles(files); }; - const handleOpenFilesModal = () => { - openFilesModal(); - }; - const handleNativeUploadClick = async () => { const files = await openFilesFromDisk({ multiple: true, - onFallbackOpen: () => fileInputRef.current?.click() + onFallbackOpen: () => fileInputRef.current?.click(), }); if (files.length > 0) { await addFiles(files); @@ -57,263 +36,48 @@ const LandingPage = () => { if (files.length > 0) { await addFiles(files); } - // Reset the input so the same file can be selected again event.target.value = ''; }; - const handleMobileUploadClick = () => { - setMobileUploadModalOpen(true); - }; - const handleFilesReceivedFromMobile = async (files: File[]) => { if (files.length > 0) { await addFiles(files); } }; - // Determine if the user has any recent files (same source as File Manager) - useEffect(() => { - let isMounted = true; - (async () => { - try { - const files = await loadRecentFiles(); - if (isMounted) { - setHasRecents((files?.length || 0) > 0); - } - } catch (_err) { - if (isMounted) setHasRecents(false); - } - })(); - return () => { isMounted = false; }; - }, [loadRecentFiles]); - return ( - - {/* White PDF Page Background */} + - {logoVariant === 'modern' && ( -
- Stirling PDF Logo -
- )} -
- {/* Logo positioned absolutely in top right corner */} + +

{t('landing.heroTitle', 'Stirling PDF')}

+

{t('landing.heroSubtitle', 'Drop in or add an existing PDF to get started.')}

- {/* Centered content container */} -
- {/* Stirling PDF Branding */} - - Stirling PDF - - - {/* Add Files + Native Upload Buttons */} -
setIsUploadHover(false)} - > - {/* Show both buttons only when recents exist; otherwise show a single Upload button */} - {hasRecents && ( - <> - - - {config?.enableMobileScanner && !isMobile && ( - - - - - - )} - - )} - {!hasRecents && ( - <> - - {config?.enableMobileScanner && !isMobile && ( - - - - - - )} - - )} -
- - {/* Hidden file input for native file picker */} - - -
- - {/* Instruction Text */} - - {terminology.dropFilesHere} - -
+ void handleNativeUploadClick()} + onMobileUploadClick={() => setMobileUploadModalOpen(true)} + onFileSelect={handleFileSelect} + />
+ setMobileUploadModalOpen(false)} diff --git a/frontend/src/core/hooks/useFileActionTerminology.ts b/frontend/src/core/hooks/useFileActionTerminology.ts index 12d992bbe2..6d5434343e 100644 --- a/frontend/src/core/hooks/useFileActionTerminology.ts +++ b/frontend/src/core/hooks/useFileActionTerminology.ts @@ -12,6 +12,8 @@ export function useFileActionTerminology() { uploadFile: t('fileUpload.uploadFile', 'Upload File'), upload: t('fileUpload.upload', 'Upload'), dropFilesHere: t('fileUpload.dropFilesHere', 'Drop files here or click the upload button'), + addFiles: t('landing.addFiles', 'Add Files'), + mobileUpload: t('landing.mobileUpload', 'Upload from Mobile'), uploadFromComputer: t('landing.uploadFromComputer', 'Upload from computer'), download: t('download', 'Download'), downloadAll: t('rightRail.downloadAll', 'Download All'), diff --git a/frontend/src/core/styles/theme.css b/frontend/src/core/styles/theme.css index ec63c0a572..7e30411d6a 100644 --- a/frontend/src/core/styles/theme.css +++ b/frontend/src/core/styles/theme.css @@ -256,6 +256,25 @@ --landing-drop-inner-paper-bg: #BBDEFB; --landing-drop-inner-paper-border: #90CAF9; + /* landing hero & stack */ + --landing-hero-gradient: linear-gradient(135deg, #4c8bf5 0%, #3a7be8 100%); + --landing-stack-w: 224px; + --landing-stack-h: 176px; + --landing-stack-glow-bg: radial-gradient(circle, rgba(74,144,226,.18) 0%, transparent 70%); + + /* landing doc stack shadows */ + --landing-doc-shadow-back-idle: 0 4px 20px rgba(0,0,0,.08), 0 1px 3px rgba(0,0,0,.04); + --landing-doc-shadow-back-hover: 0 12px 40px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.08); + --landing-doc-shadow-front-idle: 0 8px 30px rgba(0,0,0,.12), 0 4px 12px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.02); + --landing-doc-shadow-front-hover: 0 18px 48px rgba(0,0,0,.18), 0 8px 20px rgba(0,0,0,.1), 0 0 0 1px rgba(0,0,0,.04); + + /* landing action button shadows */ + --landing-action-transition: transform 0.28s cubic-bezier(0.4,0,0.2,1), box-shadow 0.32s cubic-bezier(0.4,0,0.2,1); + --landing-action-shadow-idle: 0 6px 18px rgba(0,0,0,0), 0 2px 6px rgba(0,0,0,0); + --landing-action-shadow-hover: 0 6px 18px rgba(0,0,0,.14), 0 2px 6px rgba(0,0,0,.08); + --landing-action-primary-shadow-idle: 0 10px 26px rgba(58,123,232,0), 0 4px 12px rgba(0,0,0,0); + --landing-action-primary-shadow-hover: 0 10px 26px rgba(58,123,232,.42), 0 4px 12px rgba(0,0,0,.1); + /* selected file header colors */ --header-selected-bg: #1E88E5; /* light mode selected header matches dark */ --header-selected-fg: #FFFFFF; @@ -509,15 +528,15 @@ --landing-paper-bg: #171A1F; --landing-inner-paper-bg: var(--bg-raised); --landing-inner-paper-border: #2D3237; - --landing-button-bg: #2B3037; - --landing-button-color: #ffffff; - --landing-button-border: #2D3237; - --landing-button-hover-bg: #4c525b; - /* drop state */ - --landing-drop-paper-bg: #1A2332; - --landing-drop-inner-paper-bg: #2A3441; - --landing-drop-inner-paper-border: #3A4451; + /* landing dark overrides */ + --landing-stack-glow-bg: radial-gradient(circle, rgba(30,136,229,.22) 0%, transparent 70%); + --landing-doc-shadow-back-idle: 0 4px 20px rgba(0,0,0,.35), 0 1px 3px rgba(0,0,0,.25); + --landing-doc-shadow-back-hover: 0 14px 44px rgba(0,0,0,.55), 0 6px 16px rgba(0,0,0,.35); + --landing-doc-shadow-front-idle: 0 8px 30px rgba(0,0,0,.45), 0 4px 12px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.04); + --landing-doc-shadow-front-hover: 0 20px 52px rgba(0,0,0,.6), 0 10px 24px rgba(0,0,0,.4), 0 0 0 1px rgba(255,255,255,.06); + --landing-button-color: #ffffff; + --landing-button-hover-bg: var(--bg-raised); /* selected file header colors for dark */ --header-selected-bg: #1E88E5; diff --git a/frontend/src/desktop/hooks/useFileActionTerminology.ts b/frontend/src/desktop/hooks/useFileActionTerminology.ts index 2176cedae9..fd7399533b 100644 --- a/frontend/src/desktop/hooks/useFileActionTerminology.ts +++ b/frontend/src/desktop/hooks/useFileActionTerminology.ts @@ -12,6 +12,8 @@ export function useFileActionTerminology() { uploadFile: t('fileUpload.openFile', 'Open File'), upload: t('fileUpload.open', 'Open'), dropFilesHere: t('fileUpload.dropFilesHereOpen', 'Drop files here or click the open button'), + addFiles: t('fileUpload.openFiles', 'Open Files'), + mobileUpload: t('landing.mobileUpload', 'Upload from Mobile'), uploadFromComputer: t('landing.openFromComputer', 'Open from computer'), download: t('save', 'Save'), downloadAll: t('rightRail.saveAll', 'Save All'), From 5815d0b824aa57ed4abddd532fadce769a8f6d1f Mon Sep 17 00:00:00 2001 From: "aikido-autofix[bot]" <119856028+aikido-autofix[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:50:05 +0100 Subject: [PATCH 27/59] [Aikido] Fix critical issue in axios via minor version upgrade from 1.13.6 to 1.15.0 in frontend (#6092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade axios to fix critical proxy bypass and SSRF vulnerabilities in hostname normalization that could allow attackers to reach protected internal services. ✅ There are no breaking changes
✅ 1 CVE resolved by this upgrade, including 1 critical 🚨 CVE
This PR will resolve the following CVEs: | Issue | Severity           | Description | | --- | --- | --- | |
[CVE-2025-62718](https://app.aikido.dev/issues/26490690/detail?groupId=70007#CVE-2025-62718)
|
🚨 CRITICAL
| [axios] Axios fails to properly normalize hostnames when checking NO_PROXY rules, allowing requests to loopback addresses (localhost., [::1]) to bypass proxy protections and reach internal services. This enables proxy bypass and SSRF attacks against protected loopback or internal endpoints. |
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com> --- frontend/package-lock.json | 89 +++++++++++++++++++++++++++----------- frontend/package.json | 2 +- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 24d5c95448..dbf6bbd484 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,7 +59,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@userback/widget": "^0.3.12", "autoprefixer": "^10.4.21", - "axios": "^1.13.2", + "axios": "^1.15.0", "d3": "^7.9.0", "globals": "^17.1.0", "i18next": "^25.5.2", @@ -476,6 +476,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -524,6 +525,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -586,6 +588,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.9.1.tgz", "integrity": "sha512-DlFV2o+tv9S+j4TeBVkRaIjjE9o3Tq3+hvJNoIOFtl87cR77UVQqEIRqOf61yk85Y+T2LfmnVPWjNcMuiKUh8w==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/engines": "2.9.1", "@embedpdf/models": "2.9.1" @@ -681,6 +684,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.9.1.tgz", "integrity": "sha512-aNtXjI3NUwz7kdmWsQIWzuS1QdZmuHXGCc+Kwl9u5O0PAgoj74OLsgoNEcFzz9m1rljyq3WPVnLczO6ByiifpQ==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1", "@embedpdf/utils": "2.9.1" @@ -770,6 +774,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.9.1.tgz", "integrity": "sha512-3AcvSTT7fmqe1ve/FvR3lJ5q7t5JYmnnAg8LKc9ATsDjS9J5b0WE03Omz9a8/sL19iKq8xeR1+W28phgvlcKNw==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -787,6 +792,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.9.1.tgz", "integrity": "sha512-/wpdStr1NeyMCvAEMVSCPC0a3zaMd+TSK4u8INsIo3b1RoFfb9iTlBB+qW/aaxvZJ/C7MChQ7cLX6VSKXK/6JQ==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -862,6 +868,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.9.1.tgz", "integrity": "sha512-mtfu6uDxlz3+j0xPXfKyvuu8iCFjapPkbnx8vGQ0z2PBNAMm+05hsNIzxJSGMP2VCFo09SOz2zCs7ch9J6NeNg==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -896,6 +903,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.9.1.tgz", "integrity": "sha512-+U3PSIUuNlIOTXzRhnPBP+Rx20sFOd3OPiowyI2EP/Kx/j5R/amgL/t2rjrpw9gjXEMEGsli9Fn4UqnVgMrPaQ==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -931,6 +939,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.9.1.tgz", "integrity": "sha512-dVLjiLGnZDo0xO7lZulLGl3cJ/mO7BcA3PGO2uMdhqSWK4tAF/DrakvwXdD581VBwXD/C25EJhxiNa2L7mU4wg==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1", "@embedpdf/utils": "2.9.1" @@ -1005,6 +1014,7 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.9.1.tgz", "integrity": "sha512-bVhBuZHTppKV+OB5lBLqXQv+5oW1A7kAIc5UzsImBwl6NpwH+2PdVkelfrF37yEqnEF/mdxobriWSP0aOVl93w==", "license": "MIT", + "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -1107,6 +1117,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1150,6 +1161,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2008,6 +2020,7 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", @@ -2058,6 +2071,7 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2134,6 +2148,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", "@mui/core-downloads-tracker": "^7.3.9", @@ -2581,6 +2596,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3496,6 +3512,7 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=12.16" } @@ -3591,7 +3608,6 @@ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^8.9.0" } @@ -4419,6 +4435,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5006,6 +5023,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5016,6 +5034,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5096,6 +5115,7 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -5544,7 +5564,6 @@ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "3.5.30" } @@ -5554,7 +5573,6 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.30", "@vue/shared": "3.5.30" @@ -5565,7 +5583,6 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.30", "@vue/runtime-core": "3.5.30", @@ -5578,7 +5595,6 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" @@ -5605,6 +5621,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5841,14 +5858,23 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/axobject-query": { @@ -5856,7 +5882,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">= 0.4" } @@ -6138,6 +6163,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7049,6 +7075,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -7511,15 +7538,15 @@ "version": "5.6.4", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/devtools-protocol": { "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7852,6 +7879,7 @@ "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -7961,8 +7989,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/espree": { "version": "11.2.0", @@ -8027,7 +8054,6 @@ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@typescript-eslint/types": "^8.2.0" @@ -8905,6 +8931,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.29.2" }, @@ -9185,7 +9212,6 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.6" } @@ -9361,6 +9387,7 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -9922,8 +9949,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", @@ -10943,6 +10969,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11203,6 +11230,7 @@ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.3.tgz", "integrity": "sha512-j1+MTbHO17kKXJMGDnaiW1EMOiA4AprE8EML6QnbSds+XbqHR2CdHa8T+/zIriZSoXlkZH4R+A4gY29lb5hdlA==", "license": "SEE LICENSE IN LICENSE", + "peer": true, "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", @@ -11224,6 +11252,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -11431,6 +11460,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, "license": "MIT" }, "node_modules/pump": { @@ -11598,6 +11628,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11607,6 +11638,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11689,7 +11721,8 @@ "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-number-format": { "version": "5.4.5", @@ -11706,6 +11739,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12087,7 +12121,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12937,7 +12972,6 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">= 0.4" } @@ -13210,6 +13244,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13403,6 +13438,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -13470,6 +13506,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13720,6 +13757,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13894,6 +13932,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13907,6 +13946,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -14417,8 +14457,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/zod": { "version": "3.25.76", diff --git a/frontend/package.json b/frontend/package.json index 17c031f7a5..93051e2dc7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@userback/widget": "^0.3.12", "autoprefixer": "^10.4.21", - "axios": "^1.13.2", + "axios": "^1.15.0", "d3": "^7.9.0", "globals": "^17.1.0", "i18next": "^25.5.2", From dca5787323dd275f81ff5ceb00d7374e4a802a81 Mon Sep 17 00:00:00 2001 From: "aikido-autofix[bot]" <119856028+aikido-autofix[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:54:53 +0000 Subject: [PATCH 28/59] [Aikido] Fix 16 security issues in fastmcp, aiohttp, cryptography and 1 more (#6091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade fastmcp, aiohttp, cryptography, and anthropic to fix critical SSRF/path traversal, header injection, OAuth confused deputy, and DoS vulnerabilities.
✅ 16 CVEs resolved by this upgrade, including 2 critical 🚨 CVEs
This PR will resolve the following CVEs: | Issue | Severity           | Description | | --- | --- | --- | |
[CVE-2026-32871](https://app.aikido.dev/issues/25944204/detail?groupId=70007#CVE-2026-32871)
|
🚨 CRITICAL
| [fastmcp] Path traversal vulnerability in URL construction allows attackers to bypass API prefix restrictions and access arbitrary backend endpoints using unencoded path parameters, enabling authenticated SSRF attacks. | |
[CVE-2026-27124](https://app.aikido.dev/issues/25944204/detail?groupId=70007#CVE-2026-27124)
|
HIGH
| [fastmcp] OAuthProxy fails to validate user consent when receiving authorization codes from GitHub, allowing attackers to exploit GitHub's consent-skipping behavior to gain unauthorized access to FastMCP servers through a Confused Deputy attack. | |
[CVE-2025-64340](https://app.aikido.dev/issues/25944204/detail?groupId=70007#CVE-2025-64340)
|
MEDIUM
| [fastmcp] Server names with shell metacharacters can cause command injection on Windows when passed to install commands, allowing arbitrary code execution through cmd.exe interpretation of .cmd wrapper files. | |
[CVE-2026-34520](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34520)
|
🚨 CRITICAL
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, the C parser (the default for most installs) accepted null bytes and control characters in response headers. This issue has been patched in version 3.13.4. | |
[CVE-2026-34516](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34516)
|
HIGH
| [aiohttp] A response with an excessive number of multipart headers can consume more memory than intended, leading to a denial of service (DoS) vulnerability through resource exhaustion. | |
[CVE-2026-22815](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-22815)
|
MEDIUM
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, insufficient restrictions in header/trailer handling could cause uncapped memory usage. This issue has been patched in version 3.13.4. | |
[CVE-2026-34515](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34515)
|
MEDIUM
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, on Windows the static resource handler may expose information about a NTLMv2 remote path. This issue has been patched in version 3.13.4. | |
[CVE-2026-34525](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34525)
|
MEDIUM
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, multiple Host headers were allowed in aiohttp. This issue has been patched in version 3.13.4. | |
[CVE-2026-34513](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34513)
|
LOW
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, an unbounded DNS cache could result in excessive memory usage possibly resulting in a DoS situation. This issue has been patched in version 3.13.4. | |
[CVE-2026-34514](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34514)
|
LOW
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, an attacker who controls the content_type parameter in aiohttp could use this to inject extra headers or similar exploits. This issue has been patched in version 3.13.4. | |
[CVE-2026-34517](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34517)
|
LOW
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, for some multipart form fields, aiohttp read the entire field into memory before checking client_max_size. This issue has been patched in version 3.13.4. | |
[CVE-2026-34518](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34518)
|
LOW
| [aiohttp] When following redirects to a different origin, the framework fails to drop the Cookie and Proxy-Authorization headers alongside the Authorization header, potentially leaking sensitive authentication credentials to untrusted domains. | |
[CVE-2026-34519](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34519)
|
LOW
| [aiohttp] is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, an attacker who controls the reason parameter when creating a Response may be able to inject extra headers or similar exploits. This issue has been patched in version 3.13.4. | |
[CVE-2026-39892](https://app.aikido.dev/issues/25637201/detail?groupId=70007#CVE-2026-39892)
|
MEDIUM
| [cryptography] Non-contiguous buffers passed to cryptographic APIs can cause buffer overflows, potentially leading to memory corruption and arbitrary code execution. | |
[CVE-2026-34452](https://app.aikido.dev/issues/25944200/detail?groupId=70007#CVE-2026-34452)
|
MEDIUM
| [anthropic] A time-of-check-time-of-use (TOCTOU) vulnerability in the async filesystem memory tool allows local attackers to escape the sandbox directory via symlink manipulation, enabling arbitrary file read/write operations outside the intended memory directory. | |
[CVE-2026-34450](https://app.aikido.dev/issues/25944200/detail?groupId=70007#CVE-2026-34450)
|
MEDIUM
| [anthropic] The local filesystem memory tool created world-readable and potentially world-writable files, allowing local attackers to read persisted agent state or modify memory files to influence model behavior. |
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com> --- engine/uv.lock | 206 ++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/engine/uv.lock b/engine/uv.lock index 9292fd2393..3f29481bac 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -37,7 +37,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -48,59 +48,59 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] [[package]] @@ -135,7 +135,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.86.0" +version = "0.93.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -147,9 +147,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/70/2429d6f7c2516db99fb342c3ad89575ab3e0cd31d3d2f6cba5fdf5e9c65b/anthropic-0.93.0.tar.gz", hash = "sha256:fea8376f7d5cdf99d5e8e85a48fe7a7bd8ab307cdfee4b1e8283a18b1c0ce1b5", size = 654155, upload-time = "2026-04-09T18:13:53.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/5b2c11902707c49c7a99418eb027ed3eb63876193fee5c80b5c878e3a673/anthropic-0.93.0-py3-none-any.whl", hash = "sha256:2c20b2ce6d305564c66a6cbaedddee8efdd3b9753098bf314093fcf4c662d04c", size = 627482, upload-time = "2026-04-09T18:13:51.606Z" }, ] [[package]] @@ -410,55 +410,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] @@ -636,7 +636,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.1.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -661,9 +661,9 @@ dependencies = [ { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/83/c95d3bf717698a693eccb43e137a32939d2549876e884e246028bff6ecce/fastmcp-3.1.1.tar.gz", hash = "sha256:db184b5391a31199323766a3abf3a8bfbb8010479f77eca84c0e554f18655c48", size = 17347644, upload-time = "2026-03-14T19:12:20.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/42/7eed0a38e3b7a386805fecacf8a5a9353a2b3040395ef9e30e585d8549ac/fastmcp-3.2.3.tar.gz", hash = "sha256:4f02ae8b00227285a0cf6544dea1db29b022c8cdd8d3dfdec7118540210ae60a", size = 26328743, upload-time = "2026-04-09T22:05:03.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/84b6dcba793178a44b9d99b4def6cd62f870dcfc5bb7b9153ac390135812/fastmcp-3.2.3-py3-none-any.whl", hash = "sha256:cc50af6eed1f62ed8b6ebf4987286d8d1d006f08d5bec739d5c7fb76160e0911", size = 707260, upload-time = "2026-04-09T22:05:01.225Z" }, ] [[package]] From df08ad074904903450cc90814dc7a9d816d89046 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 10 Apr 2026 17:41:19 +0100 Subject: [PATCH 29/59] Add frontend autoformatting and set CI to require formatted code for all languages (#6052) # Description of Changes Changes the strategy for autoformatting to reject PRs if they are not formatted correctly instead of allowing them to merge and then spawning a new PR to fix the formatting. The old strategy just caused more work for us because we'd have to manually approve the followup PR and get it merged, which required 2 reviewers so in practice it rarely got done and just meant everyone's PRs ended up containing reformatting for unrelated files, which makes code review unnecessarily difficult. If the PR's code is not formatted correctly after this PR, a comment will be added automatically to tell the author how to run the formatter script to fix their code so it can go in. This also enables autoformatting for the frontend code, using Prettier. I've enabled it for pretty much everything in the frontend folder, other than 3rd party files and files it doesn't make sense for. I also excluded Markdown because it sounds likely to be more annoying to have to autoformat the Markdown in the frontend folder but nowhere else. Open to changing this though if people disagree. > [!note] > > Advice to reviewers: The first commit contains all of the actual logic I've introduced (CI changes, Prettier config, etc.) > The second commit is just the reformatting of the entire frontend folder. > The first commit needs proper review, the second one just give it a spot-check that it's doing what you'd expect. --- .github/workflows/build.yml | 104 + .github/workflows/pre_commit.yml | 57 +- build.gradle | 4 +- frontend/.prettierignore | 10 + frontend/eslint.config.mjs | 86 +- frontend/index.html | 7 +- frontend/package-lock.json | 17 + frontend/package.json | 5 + frontend/playwright.config.ts | 34 +- frontend/postcss.config.js | 5 +- .../public/css/cookieconsentCustomisation.css | 241 +- frontend/public/manifest-classic.json | 1 - frontend/scripts/build-provisioner.mjs | 24 +- frontend/scripts/generate-icons.js | 60 +- frontend/scripts/generate-licenses.js | 766 +++---- frontend/scripts/sample-pdf/generate.mjs | 51 +- frontend/scripts/sample-pdf/styles.css | 5 +- frontend/scripts/sample-pdf/template.html | 410 ++-- frontend/scripts/setup-env.ts | 41 +- frontend/src-tauri/capabilities/default.json | 4 +- frontend/src-tauri/tauri.conf.json | 170 +- frontend/src/assets/3rdPartyLicenses.json | 650 +++--- frontend/src/core/App.tsx | 4 +- frontend/src/core/components/AppLayout.tsx | 12 +- frontend/src/core/components/AppProviders.tsx | 88 +- frontend/src/core/components/FileManager.tsx | 162 +- .../src/core/components/StorageStatsCard.tsx | 16 +- .../providers/PDFAnnotationProvider.tsx | 16 +- .../annotation/shared/BaseAnnotationTool.tsx | 32 +- .../annotation/shared/ColorControl.tsx | 48 +- .../annotation/shared/ColorPicker.tsx | 52 +- .../annotation/shared/DrawingCanvas.tsx | 116 +- .../annotation/shared/DrawingControls.tsx | 32 +- .../annotation/shared/ImageUploader.tsx | 128 +- .../annotation/shared/OpacityControl.tsx | 34 +- .../annotation/shared/PropertiesPopover.tsx | 89 +- .../annotation/shared/TextInputWithFont.tsx | 69 +- .../annotation/shared/WidthControl.tsx | 34 +- .../annotation/tools/DrawingTool.tsx | 27 +- .../components/annotation/tools/ImageTool.tsx | 27 +- .../components/fileEditor/AddFileCard.tsx | 135 +- .../fileEditor/FileEditor.module.css | 33 +- .../core/components/fileEditor/FileEditor.tsx | 588 ++--- .../fileEditor/FileEditorFileName.tsx | 10 +- .../fileEditor/FileEditorThumbnail.tsx | 548 +++-- .../fileEditor/fileEditorRightRailButtons.tsx | 81 +- .../fileManager/CompactFileDetails.tsx | 102 +- .../components/fileManager/DesktopLayout.tsx | 140 +- .../components/fileManager/DragOverlay.tsx | 26 +- .../fileManager/EmptyFilesState.tsx | 114 +- .../components/fileManager/FileActions.tsx | 20 +- .../components/fileManager/FileDetails.tsx | 40 +- .../fileManager/FileHistoryGroup.tsx | 14 +- .../components/fileManager/FileInfoCard.tsx | 152 +- .../components/fileManager/FileListArea.tsx | 39 +- .../components/fileManager/FileListItem.tsx | 226 +- .../fileManager/FileSourceButtons.tsx | 126 +- .../fileManager/HiddenFileInput.tsx | 6 +- .../components/fileManager/MobileLayout.tsx | 90 +- .../components/fileManager/SearchInput.tsx | 23 +- .../core/components/hotkeys/HotkeyDisplay.tsx | 50 +- .../src/core/components/layout/Workbench.tsx | 160 +- .../InitialOnboardingModal.module.css | 38 +- .../InitialOnboardingModal/renderButtons.tsx | 58 +- .../core/components/onboarding/Onboarding.tsx | 432 ++-- .../onboarding/OnboardingModalSlide.tsx | 73 +- .../onboarding/OnboardingStepper.tsx | 14 +- .../components/onboarding/OnboardingTour.css | 7 +- .../components/onboarding/OnboardingTour.tsx | 59 +- .../components/onboarding/adminStepsConfig.ts | 124 +- .../onboarding/onboardingFlowConfig.ts | 344 ++- .../orchestrator/onboardingConfig.ts | 100 +- .../orchestrator/onboardingStorage.ts | 34 +- .../orchestrator/useOnboardingOrchestrator.ts | 129 +- .../slides/AnalyticsChoiceSlide.tsx | 35 +- .../slides/AnimatedSlideBackground.tsx | 52 +- .../onboarding/slides/DesktopInstallSlide.tsx | 22 +- .../onboarding/slides/DesktopInstallTitle.tsx | 65 +- .../onboarding/slides/FirstLoginSlide.tsx | 88 +- .../onboarding/slides/MFASetupSlide.tsx | 22 +- .../onboarding/slides/PlanOverviewSlide.tsx | 42 +- .../onboarding/slides/SecurityCheckSlide.tsx | 46 +- .../onboarding/slides/ServerLicenseSlide.tsx | 20 +- .../onboarding/slides/TourOverviewSlide.tsx | 19 +- .../onboarding/slides/WelcomeSlide.tsx | 17 +- .../slides/unifiedBackgroundConfig.ts | 11 +- .../core/components/onboarding/tourGlow.ts | 9 +- .../onboarding/useBypassOnboarding.ts | 20 +- .../onboarding/useOnboardingDownload.ts | 49 +- .../onboarding/useOnboardingEffects.ts | 22 +- .../components/onboarding/userStepsConfig.ts | 103 +- .../onboarding/whatsNewStepsConfig.ts | 62 +- .../pageEditor/BulkSelectionPanel.tsx | 24 +- .../components/pageEditor/DragDropGrid.tsx | 455 ++-- .../components/pageEditor/FileThumbnail.tsx | 213 +- .../pageEditor/PageEditor.module.css | 21 +- .../core/components/pageEditor/PageEditor.tsx | 493 +++-- .../pageEditor/PageEditorControls.tsx | 86 +- .../pageEditor/PageSelectByNumberButton.tsx | 12 +- .../components/pageEditor/PageThumbnail.tsx | 492 +++-- .../AdvancedSelectionPanel.tsx | 63 +- .../BulkSelectionPanel.module.css | 55 +- .../bulkSelectionPanel/OperatorsSection.tsx | 74 +- .../bulkSelectionPanel/PageSelectionInput.tsx | 45 +- .../bulkSelectionPanel/SelectPages.tsx | 39 +- .../SelectedPagesDisplay.tsx | 29 +- .../pageEditor/commands/pageCommands.ts | 186 +- .../core/components/pageEditor/constants.ts | 8 +- .../core/components/pageEditor/fileColors.ts | 42 +- .../hooks/useEditedDocumentState.ts | 29 +- .../pageEditor/hooks/useEditorCommands.ts | 106 +- .../pageEditor/hooks/useFileColorMap.ts | 6 +- .../hooks/useInitialPageDocument.ts | 8 +- .../pageEditor/hooks/usePageDocument.ts | 170 +- .../hooks/usePageEditorDropdownState.ts | 45 +- .../pageEditor/hooks/usePageEditorExport.ts | 96 +- .../pageEditor/hooks/usePageEditorState.ts | 34 +- .../hooks/usePageSelectionManager.ts | 18 +- .../pageEditor/hooks/useUndoManagerState.ts | 6 +- .../pageEditor/pageEditorRightRailButtons.tsx | 52 +- .../pageEditor/utils/splitPositions.ts | 2 +- .../components/shared/AllToolsNavButton.tsx | 28 +- .../components/shared/AppConfigLoader.tsx | 6 +- .../core/components/shared/AppConfigModal.css | 2 +- .../core/components/shared/AppConfigModal.tsx | 132 +- frontend/src/core/components/shared/Badge.tsx | 58 +- .../core/components/shared/BulkShareModal.tsx | 147 +- .../shared/BulkUploadToServerModal.tsx | 81 +- .../components/shared/ButtonSelector.test.tsx | 185 +- .../core/components/shared/ButtonSelector.tsx | 54 +- .../core/components/shared/ButtonToggle.tsx | 44 +- .../core/components/shared/CardSelector.tsx | 103 +- .../shared/DismissAllErrorsButton.tsx | 32 +- .../shared/DropdownListWithFooter.tsx | 214 +- .../components/shared/EditableSecretField.tsx | 45 +- .../shared/EncryptedPdfUnlockModal.tsx | 28 +- .../core/components/shared/ErrorBoundary.tsx | 84 +- .../src/core/components/shared/FileCard.tsx | 72 +- .../components/shared/FileDropdownMenu.tsx | 52 +- .../src/core/components/shared/FileGrid.tsx | 92 +- .../components/shared/FilePickerModal.tsx | 111 +- .../core/components/shared/FilePreview.tsx | 83 +- .../components/shared/FileUploadButton.tsx | 14 +- .../components/shared/FirstLoginModal.tsx | 77 +- .../src/core/components/shared/FitText.tsx | 32 +- .../src/core/components/shared/Footer.tsx | 166 +- .../components/shared/HoverActionMenu.tsx | 23 +- .../src/core/components/shared/InfoBanner.tsx | 51 +- .../core/components/shared/LandingActions.tsx | 41 +- .../shared/LandingDocumentStack.tsx | 6 +- .../core/components/shared/LandingPage.tsx | 42 +- .../shared/LanguageSelector.module.css | 10 +- .../components/shared/LanguageSelector.tsx | 187 +- .../src/core/components/shared/LocalIcon.tsx | 22 +- .../components/shared/MobileUploadModal.tsx | 192 +- .../components/shared/MultiSelectControls.tsx | 36 +- .../shared/NavigationWarningModal.tsx | 74 +- .../components/shared/ObscuredOverlay.tsx | 12 +- .../shared/PageEditorFileDropdown.tsx | 143 +- .../shared/PageSelectionSyntaxHint.tsx | 30 +- .../core/components/shared/PrivateContent.tsx | 13 +- .../core/components/shared/QuickAccessBar.tsx | 954 ++++---- .../shared/RainbowThemeProvider.tsx | 33 +- .../src/core/components/shared/RightRail.tsx | 160 +- .../core/components/shared/ShareFileModal.tsx | 133 +- .../shared/ShareManagementModal.tsx | 634 +++--- .../core/components/shared/SkeletonLoader.tsx | 73 +- .../src/core/components/shared/TextInput.tsx | 160 +- .../src/core/components/shared/ToolChain.tsx | 107 +- .../src/core/components/shared/ToolIcon.tsx | 4 +- .../src/core/components/shared/Tooltip.tsx | 140 +- .../core/components/shared/TopControls.tsx | 173 +- .../core/components/shared/UpdateModal.tsx | 172 +- .../components/shared/UploadToServerModal.tsx | 72 +- .../core/components/shared/UserSelector.tsx | 63 +- .../components/shared/ZipWarningModal.tsx | 8 +- .../shared/config/LoginRequiredBanner.tsx | 22 +- .../shared/config/OverviewHeader.tsx | 10 +- .../components/shared/config/PendingBadge.tsx | 10 +- .../config/RestartConfirmationModal.tsx | 43 +- .../shared/config/SettingsSearchBar.tsx | 128 +- .../shared/config/SettingsStickyFooter.tsx | 18 +- .../shared/config/configNavSections.tsx | 52 +- .../config/configSections/GeneralSection.tsx | 6 +- .../config/configSections/HotkeysSection.tsx | 187 +- .../shared/config/configSections/Overview.tsx | 82 +- .../config/configSections/ProviderCard.tsx | 98 +- .../configSections/providerDefinitions.ts | 736 ++++--- .../core/components/shared/config/types.ts | 62 +- .../shared/config/useRestartServer.ts | 55 +- .../shared/filePreview/DocumentStack.tsx | 53 +- .../shared/filePreview/DocumentThumbnail.tsx | 54 +- .../shared/filePreview/HoverOverlay.tsx | 48 +- .../shared/filePreview/NavigationArrows.tsx | 37 +- .../core/components/shared/fitText/textFit.ts | 40 +- .../shared/pageEditor/useFileItemDragDrop.ts | 68 +- .../quickAccessBar/ActiveToolButton.tsx | 68 +- .../shared/quickAccessBar/QuickAccessBar.css | 36 +- .../shared/quickAccessBar/QuickAccessBar.ts | 46 +- .../quickAccessBar/QuickAccessButton.tsx | 65 +- .../shared/quickAccessBar/useToursTooltip.ts | 15 +- .../components/shared/rightRail/RightRail.css | 4 +- .../rightRail/ViewerAnnotationControls.tsx | 96 +- .../shared/signing/ActiveSessionsPanel.tsx | 59 +- .../shared/signing/CompletedSessionsPanel.tsx | 42 +- .../shared/signing/CreateSessionFlow.tsx | 127 +- .../shared/signing/CreateSessionPanel.tsx | 27 +- .../components/shared/signing/SignPopout.tsx | 572 +++-- .../steps/ConfigureSignatureDefaultsStep.tsx | 41 +- .../signing/steps/ReviewSessionStep.tsx | 66 +- .../signing/steps/SelectDocumentStep.tsx | 37 +- .../signing/steps/SelectParticipantsStep.tsx | 23 +- .../sliderWithInput/SliderWithInput.tsx | 10 +- .../shared/tooltip/Tooltip.module.css | 25 +- .../shared/tooltip/TooltipContent.tsx | 69 +- .../wetSignature/DrawSignatureCanvas.tsx | 61 +- .../wetSignature/SignatureTypeSelector.tsx | 24 +- .../shared/wetSignature/TypeSignatureText.tsx | 73 +- .../wetSignature/UploadSignatureImage.tsx | 55 +- .../core/components/toast/ToastContext.tsx | 179 +- .../core/components/toast/ToastRenderer.tsx | 95 +- frontend/src/core/components/toast/index.ts | 15 +- frontend/src/core/components/toast/types.ts | 10 +- .../components/tools/FullscreenToolList.tsx | 102 +- .../tools/FullscreenToolSurface.tsx | 79 +- .../core/components/tools/SearchResults.tsx | 38 +- .../src/core/components/tools/ToolPanel.css | 45 +- .../src/core/components/tools/ToolPanel.tsx | 151 +- .../components/tools/ToolPanelModePrompt.css | 18 +- .../components/tools/ToolPanelModePrompt.tsx | 78 +- .../src/core/components/tools/ToolPicker.tsx | 124 +- .../core/components/tools/ToolRenderer.tsx | 18 +- .../addAttachments/AddAttachmentsSettings.tsx | 55 +- .../AddPageNumbersAppearanceSettings.tsx | 60 +- .../AddPageNumbersAutomationSettings.tsx | 16 +- .../AddPageNumbersPositionSettings.tsx | 32 +- .../PageNumberPreview.module.css | 53 +- .../addPageNumbers/PageNumberPreview.tsx | 107 +- .../useAddPageNumbersOperation.ts | 34 +- .../useAddPageNumbersParameters.ts | 20 +- .../addPassword/AddPasswordSettings.test.tsx | 143 +- .../tools/addPassword/AddPasswordSettings.tsx | 23 +- .../addStamp/AddStampAutomationSettings.tsx | 6 +- .../StampPositionFormattingSettings.tsx | 115 +- .../tools/addStamp/StampPreview.tsx | 168 +- .../tools/addStamp/StampPreviewUtils.ts | 140 +- .../tools/addStamp/StampSetupSettings.tsx | 315 +-- .../tools/addStamp/useAddStampOperation.ts | 51 +- .../tools/addStamp/useAddStampParameters.ts | 40 +- .../AddWatermarkSingleStepSettings.tsx | 30 +- .../addWatermark/WatermarkFormatting.tsx | 26 +- .../tools/addWatermark/WatermarkImageFile.tsx | 4 +- .../addWatermark/WatermarkStyleSettings.tsx | 33 +- .../tools/addWatermark/WatermarkTextStyle.tsx | 1 - .../addWatermark/WatermarkTypeSettings.tsx | 12 +- .../tools/addWatermark/WatermarkWording.tsx | 4 +- .../AdjustContrastBasicSettings.tsx | 31 +- .../AdjustContrastColorSettings.tsx | 31 +- .../adjustContrast/AdjustContrastPreview.tsx | 62 +- .../AdjustContrastSingleStepSettings.tsx | 22 +- .../components/tools/adjustContrast/utils.ts | 48 +- .../AdjustPageScaleSettings.test.tsx | 46 +- .../AdjustPageScaleSettings.tsx | 28 +- .../tools/autoRename/AutoRenameSettings.tsx | 14 +- .../tools/automate/AutomationCreation.tsx | 130 +- .../tools/automate/AutomationEntry.tsx | 107 +- .../tools/automate/AutomationRun.tsx | 55 +- .../tools/automate/AutomationSelection.tsx | 98 +- .../tools/automate/IconSelector.tsx | 5 +- .../tools/automate/ToolConfigurationModal.tsx | 64 +- .../components/tools/automate/ToolList.tsx | 9 +- .../tools/automate/ToolSelector.tsx | 144 +- .../core/components/tools/automate/iconMap.ts | 116 +- .../BookletImpositionSettings.tsx | 98 +- .../certSign/CertSignAutomationSettings.tsx | 28 +- .../certSign/CertificateFilesSettings.tsx | 59 +- .../certSign/CertificateFormatSettings.tsx | 57 +- .../tools/certSign/CertificateSelector.tsx | 74 +- .../certSign/CertificateTypeSettings.tsx | 36 +- .../certSign/SessionDetailWorkbenchView.tsx | 173 +- .../certSign/SignControlsStrip.module.css | 8 +- .../tools/certSign/SignControlsStrip.tsx | 315 ++- .../certSign/SignRequestWorkbenchView.tsx | 185 +- .../certSign/SignatureAppearanceSettings.tsx | 80 +- .../certSign/SignatureSettingsDisplay.tsx | 43 +- .../tools/certSign/SignatureSettingsInput.tsx | 63 +- .../tools/certSign/WetSignatureInput.tsx | 119 +- .../certSign/modals/AddParticipantsFlow.tsx | 39 +- .../modals/CertificateConfigModal.tsx | 144 +- .../certSign/modals/SelectSignatureModal.tsx | 99 +- .../certSign/panels/ParticipantListPanel.tsx | 48 +- .../certSign/panels/SessionActionsPanel.tsx | 43 +- .../certSign/steps/AddSignaturesStep.tsx | 81 +- .../steps/CertificateSelectionStep.tsx | 17 +- .../certSign/steps/ReviewSignatureStep.tsx | 65 +- .../certSign/steps/SignatureCreationStep.tsx | 44 +- .../certSign/steps/SignaturePlacementStep.tsx | 22 +- .../ChangeMetadataSingleStep.tsx | 39 +- .../steps/AdvancedOptionsStep.tsx | 12 +- .../steps/CustomMetadataStep.tsx | 29 +- .../changeMetadata/steps/DeleteAllStep.tsx | 10 +- .../steps/DocumentDatesStep.tsx | 18 +- .../steps/StandardMetadataStep.tsx | 42 +- .../ChangePermissionsSettings.test.tsx | 181 +- .../tools/compare/CompareDocumentPane.tsx | 82 +- .../compare/CompareNavigationDropdown.tsx | 157 +- .../tools/compare/CompareWorkbenchView.tsx | 423 ++-- .../core/components/tools/compare/compare.ts | 52 +- .../components/tools/compare/compareView.css | 22 +- .../hooks/useCompareChangeNavigation.ts | 87 +- .../compare/hooks/useCompareHighlights.ts | 29 +- .../compare/hooks/useComparePagePreviews.ts | 40 +- .../tools/compare/hooks/useComparePanZoom.ts | 318 +-- .../hooks/useCompareRightRailButtons.tsx | 277 ++- .../tools/compress/CompressSettings.tsx | 111 +- .../tools/convert/ConvertFromCbrSettings.tsx | 26 +- .../tools/convert/ConvertFromCbzSettings.tsx | 28 +- .../convert/ConvertFromEbookSettings.tsx | 24 +- .../convert/ConvertFromEmailSettings.tsx | 60 +- .../convert/ConvertFromImageSettings.tsx | 57 +- .../tools/convert/ConvertFromSvgSettings.tsx | 32 +- .../tools/convert/ConvertFromWebSettings.tsx | 40 +- .../tools/convert/ConvertSettings.tsx | 210 +- .../tools/convert/ConvertToCbrSettings.tsx | 13 +- .../tools/convert/ConvertToCbzSettings.tsx | 29 +- .../tools/convert/ConvertToEpubSettings.tsx | 57 +- .../tools/convert/ConvertToImageSettings.tsx | 43 +- .../tools/convert/ConvertToPdfaSettings.tsx | 56 +- .../tools/convert/ConvertToPdfxSettings.tsx | 14 +- .../tools/convert/GroupedFormatDropdown.tsx | 83 +- .../tools/crop/CropAreaSelector.tsx | 238 +- .../tools/crop/CropAutomationSettings.tsx | 4 +- .../tools/crop/CropCoordinateInputs.tsx | 20 +- .../components/tools/crop/CropSettings.tsx | 52 +- .../editTableOfContents/BookmarkEditor.tsx | 125 +- .../EditTableOfContentsSettings.tsx | 97 +- .../EditTableOfContentsWorkbenchView.tsx | 87 +- .../extractImages/ExtractImagesSettings.tsx | 28 +- .../extractPages/ExtractPagesSettings.tsx | 12 +- .../tools/flatten/FlattenSettings.tsx | 22 +- .../tools/fullscreen/CompactToolItem.tsx | 89 +- .../tools/fullscreen/DetailedToolItem.tsx | 53 +- .../components/tools/fullscreen/shared.ts | 85 +- .../tools/getPdfInfo/GetPdfInfoReportView.tsx | 133 +- .../tools/getPdfInfo/GetPdfInfoResults.tsx | 30 +- .../getPdfInfo/sections/ComplianceSection.tsx | 122 +- .../getPdfInfo/sections/KeyValueSection.tsx | 8 +- .../getPdfInfo/sections/OtherSection.tsx | 91 +- .../getPdfInfo/sections/PerPageSection.tsx | 125 +- .../getPdfInfo/sections/SummarySection.tsx | 162 +- .../sections/TableOfContentsSection.tsx | 22 +- .../tools/getPdfInfo/shared/KeyValueList.tsx | 22 +- .../getPdfInfo/shared/ScrollableCodeBlock.tsx | 23 +- .../tools/getPdfInfo/shared/SectionBlock.tsx | 10 +- .../getPdfInfo/shared/accordionStyles.ts | 9 +- .../tools/merge/MergeFileSorter.test.tsx | 110 +- .../tools/merge/MergeFileSorter.tsx | 46 +- .../tools/merge/MergeSettings.test.tsx | 76 +- .../components/tools/merge/MergeSettings.tsx | 22 +- .../tools/ocr/AdvancedOCRSettings.tsx | 38 +- .../tools/ocr/LanguagePicker.module.css | 4 +- .../components/tools/ocr/LanguagePicker.tsx | 104 +- .../core/components/tools/ocr/OCRSettings.tsx | 35 +- .../tools/overlayPdfs/OverlayPdfsSettings.tsx | 152 +- .../tools/pageLayout/LayoutPreview.tsx | 33 +- .../pageLayout/PageLayoutAdvancedSettings.tsx | 38 +- .../PageLayoutMarginsBordersSettings.tsx | 86 +- .../tools/pageLayout/PageLayoutPreview.tsx | 15 +- .../tools/pageLayout/PageLayoutSettings.tsx | 127 +- .../components/tools/pageLayout/constants.ts | 20 +- .../tools/pageLayout/utils/computeBoxes.ts | 14 +- .../tools/pdfTextEditor/FontStatusPanel.tsx | 163 +- .../pdfTextEditor/PdfTextEditorSidebar.tsx | 161 +- .../tools/pdfTextEditor/PdfTextEditorView.tsx | 1917 ++++++++-------- .../tools/redact/ManualRedactionControls.tsx | 35 +- .../redact/RedactAdvancedSettings.test.tsx | 189 +- .../tools/redact/RedactAdvancedSettings.tsx | 20 +- .../tools/redact/RedactModeSelector.tsx | 25 +- .../redact/RedactSingleStepSettings.test.tsx | 153 +- .../tools/redact/RedactSingleStepSettings.tsx | 16 +- .../tools/redact/WordsToRedactInput.test.tsx | 164 +- .../tools/redact/WordsToRedactInput.tsx | 58 +- .../RemoveAnnotationsSettings.tsx | 10 +- .../removeBlanks/RemoveBlanksSettings.tsx | 20 +- .../RemoveCertificateSignSettings.tsx | 13 +- .../tools/removePages/RemovePagesSettings.tsx | 21 +- .../RemovePasswordSettings.test.tsx | 111 +- .../removePassword/RemovePasswordSettings.tsx | 6 +- .../ReorganizePagesSettings.tsx | 55 +- .../tools/reorganizePages/constants.ts | 111 +- .../tools/repair/RepairSettings.tsx | 11 +- .../replaceColor/ReplaceColorSettings.tsx | 64 +- .../tools/rotate/RotateAutomationSettings.tsx | 2 +- .../tools/rotate/RotateSettings.tsx | 47 +- .../tools/sanitize/SanitizeSettings.test.tsx | 129 +- .../tools/sanitize/SanitizeSettings.tsx | 11 +- .../ScannerImageSplitSettings.tsx | 51 +- .../tools/shared/ErrorNotification.tsx | 19 +- .../components/tools/shared/FileMetadata.tsx | 6 +- .../tools/shared/FileStatusIndicator.tsx | 48 +- .../components/tools/shared/FilesToolStep.tsx | 29 +- .../tools/shared/NavigationControls.tsx | 35 +- .../components/tools/shared/NoToolsFound.tsx | 8 +- .../tools/shared/NumberInputWithUnit.tsx | 12 +- .../tools/shared/OperationButton.tsx | 69 +- .../tools/shared/ResultsPreview.tsx | 40 +- .../tools/shared/ReviewToolStep.tsx | 10 +- .../tools/shared/ScopedOperationButton.tsx | 37 +- .../tools/shared/SubcategoryHeader.tsx | 6 +- .../tools/shared/SuggestedToolsSection.tsx | 25 +- .../core/components/tools/shared/ToolStep.tsx | 162 +- .../tools/shared/ToolWorkflowTitle.tsx | 30 +- .../tools/shared/createToolFlow.tsx | 119 +- .../tools/shared/renderToolButtons.tsx | 16 +- .../components/tools/sign/PenSizeSelector.tsx | 10 +- .../tools/sign/SavedSignaturesSection.tsx | 196 +- .../components/tools/sign/SignSettings.tsx | 615 +++--- .../SingleLargePageSettings.tsx | 11 +- .../tools/split/SplitAutomationSettings.tsx | 8 +- .../components/tools/split/SplitSettings.tsx | 90 +- .../timestampPdf/TimestampPdfSettings.tsx | 27 +- .../tools/toolPicker/FavoriteStar.tsx | 16 +- .../tools/toolPicker/ToolButton.tsx | 114 +- .../tools/toolPicker/ToolPicker.css | 2 +- .../tools/toolPicker/ToolSearch.tsx | 26 +- .../unlockPdfForms/UnlockPdfFormsSettings.tsx | 11 +- .../ValidateSignatureReportView.tsx | 54 +- .../ValidateSignatureResults.tsx | 106 +- .../ValidateSignatureSettings.tsx | 38 +- .../reportView/FieldBlock.tsx | 11 +- .../reportView/FileSummaryHeader.tsx | 23 +- .../reportView/SignatureSection.tsx | 62 +- .../reportView/SignatureStatusBadge.tsx | 29 +- .../reportView/ThumbnailPreview.tsx | 18 +- .../PageLayout/usePageLayoutAdvancedTips.ts | 32 +- .../usePageLayoutMarginsBordersTips.ts | 30 +- .../tooltips/PageLayout/usePageLayoutTips.ts | 26 +- .../tooltips/useAddAttachmentsTips.ts | 15 +- .../tooltips/useAddPasswordPermissionsTips.ts | 15 +- .../components/tooltips/useAddPasswordTips.ts | 29 +- .../tooltips/useAdjustPageScaleTips.ts | 29 +- .../components/tooltips/useAdvancedOCRTips.ts | 33 +- .../components/tooltips/useAutoRenameTips.ts | 19 +- .../tooltips/useBookletImpositionTips.ts | 33 +- .../tooltips/useCertSignTooltips.ts | 39 +- .../tooltips/useCertificateChoiceTips.ts | 48 +- .../tooltips/useCertificateTypeTips.ts | 26 +- .../tooltips/useChangeMetadataTips.ts | 57 +- .../tooltips/useChangePermissionsTips.ts | 20 +- .../components/tooltips/useCompressTips.ts | 33 +- .../components/tooltips/useCropTooltips.ts | 15 +- .../tooltips/useExtractPagesTips.ts | 14 +- .../components/tooltips/useFlattenTips.ts | 30 +- .../tooltips/useGroupSigningTips.ts | 51 +- .../core/components/tooltips/useMergeTips.tsx | 24 +- .../core/components/tooltips/useOCRTips.ts | 26 +- .../components/tooltips/useOverlayPdfsTips.ts | 64 +- .../tooltips/usePageSelectionTips.ts | 56 +- .../tooltips/usePageSelectionTips.tsx | 39 +- .../tooltips/usePdfTextEditorTips.ts | 25 +- .../core/components/tooltips/useRedactTips.ts | 95 +- .../tooltips/useRemoveAnnotationsTips.ts | 15 +- .../tooltips/useRemoveBlanksTips.ts | 35 +- .../components/tooltips/useRemovePagesTips.ts | 21 +- .../tooltips/useRemovePasswordTips.ts | 14 +- .../tooltips/useReplaceColorTips.ts | 43 +- .../core/components/tooltips/useRotateTips.ts | 14 +- .../tooltips/useScannerImageSplitTips.ts | 68 +- .../tooltips/useSessionManagementTips.ts | 65 +- .../components/tooltips/useSignModeTips.ts | 43 +- .../tooltips/useSignatureAppearanceTips.ts | 30 +- .../tooltips/useSignatureSettingsTips.ts | 54 +- .../components/tooltips/useSplitMethodTips.ts | 21 +- .../tooltips/useSplitSettingsTips.ts | 128 +- .../components/tooltips/useWatermarkTips.ts | 113 +- .../tooltips/useWetSignatureTips.ts | 42 +- .../viewer/ActiveDocumentContext.tsx | 10 +- .../components/viewer/AnnotationAPIBridge.tsx | 78 +- .../viewer/AnnotationMenuButtons.tsx | 68 +- .../viewer/AnnotationSelectionMenu.tsx | 88 +- .../viewer/AnnotationTypeButtons.tsx | 90 +- .../components/viewer/AttachmentAPIBridge.tsx | 95 +- .../components/viewer/AttachmentSidebar.tsx | 131 +- .../components/viewer/BookmarkAPIBridge.tsx | 26 +- .../components/viewer/BookmarkSidebar.tsx | 174 +- .../components/viewer/CommentsSidebar.tsx | 433 ++-- .../components/viewer/CustomSearchLayer.tsx | 59 +- .../viewer/DocumentPermissionsAPIBridge.tsx | 61 +- .../viewer/DocumentReadyWrapper.tsx | 6 +- .../core/components/viewer/EmbedPdfViewer.tsx | 511 +++-- .../components/viewer/ExportAPIBridge.tsx | 14 +- .../components/viewer/HistoryAPIBridge.tsx | 114 +- .../core/components/viewer/LayerSidebar.css | 4 +- .../core/components/viewer/LayerSidebar.tsx | 110 +- .../src/core/components/viewer/LinkLayer.tsx | 81 +- .../core/components/viewer/LocalEmbedPDF.tsx | 1402 ++++++------ .../viewer/LocalEmbedPDFWithAnnotations.tsx | 1317 +++++------ .../core/components/viewer/NonPdfViewer.tsx | 72 +- .../core/components/viewer/PanAPIBridge.tsx | 20 +- .../components/viewer/PdfViewerToolbar.tsx | 375 ++-- .../core/components/viewer/PrintAPIBridge.tsx | 14 +- .../components/viewer/RedactionAPIBridge.tsx | 76 +- .../viewer/RedactionPendingTracker.tsx | 55 +- .../viewer/RedactionSelectionMenu.tsx | 171 +- .../components/viewer/RotateAPIBridge.tsx | 18 +- .../core/components/viewer/RulerOverlay.tsx | 496 +++-- .../components/viewer/ScrollAPIBridge.tsx | 16 +- .../components/viewer/SearchAPIBridge.tsx | 67 +- .../components/viewer/SearchInterface.tsx | 66 +- .../components/viewer/SelectionAPIBridge.tsx | 46 +- .../components/viewer/SignatureAPIBridge.tsx | 400 ++-- .../viewer/SignatureFieldOverlay.tsx | 107 +- .../viewer/SignaturePlacementOverlay.tsx | 46 +- .../components/viewer/SpreadAPIBridge.tsx | 14 +- .../viewer/StampPlacementOverlay.tsx | 52 +- .../viewer/TextSelectionHandler.tsx | 61 +- .../components/viewer/ThumbnailAPIBridge.tsx | 14 +- .../components/viewer/ThumbnailSidebar.tsx | 167 +- .../src/core/components/viewer/Viewer.tsx | 10 +- .../core/components/viewer/ZoomAPIBridge.tsx | 51 +- .../components/viewer/constants/search.ts | 8 +- .../viewer/hooks/useDocumentReady.ts | 8 +- .../src/core/components/viewer/layerUtils.ts | 52 +- .../components/viewer/nonpdf/CsvViewer.tsx | 100 +- .../components/viewer/nonpdf/HtmlViewer.tsx | 20 +- .../components/viewer/nonpdf/ImageViewer.tsx | 22 +- .../components/viewer/nonpdf/JsonViewer.tsx | 42 +- .../components/viewer/nonpdf/NonPdfBanner.tsx | 24 +- .../components/viewer/nonpdf/TextViewer.tsx | 173 +- .../core/components/viewer/nonpdf/types.ts | 56 +- .../viewer/readAloudHighlight.test.ts | 22 +- .../components/viewer/useActiveDocumentId.ts | 2 +- .../viewer/useAnnotationMenuHandlers.ts | 276 +-- .../viewer/useStopReadAloudOnNavigation.ts | 15 +- .../components/viewer/useViewerReadAloud.ts | 741 ++++--- .../viewer/useViewerRightRailButtons.tsx | 306 +-- .../src/core/components/viewer/viewerTypes.ts | 59 +- frontend/src/core/constants/app.ts | 17 +- frontend/src/core/constants/automation.ts | 42 +- .../src/core/constants/convertConstants.ts | 437 ++-- .../core/constants/convertSupportedFornats.ts | 86 +- frontend/src/core/constants/cropConstants.ts | 3 +- frontend/src/core/constants/downloads.ts | 11 +- frontend/src/core/constants/events.ts | 31 +- frontend/src/core/constants/logo.ts | 9 +- frontend/src/core/constants/routes.ts | 10 +- frontend/src/core/constants/signConstants.ts | 12 +- frontend/src/core/constants/splitConstants.ts | 57 +- frontend/src/core/constants/theme.ts | 6 +- frontend/src/core/constants/toolPanel.ts | 4 +- .../AdminTourOrchestrationContext.tsx | 43 +- .../src/core/contexts/AnnotationContext.tsx | 6 +- .../core/contexts/AppConfigContext.test.tsx | 88 +- .../src/core/contexts/AppConfigContext.tsx | 184 +- frontend/src/core/contexts/BannerContext.tsx | 10 +- .../core/contexts/CommentAuthorContext.tsx | 18 +- frontend/src/core/contexts/FileContext.tsx | 487 +++-- .../src/core/contexts/FileManagerContext.tsx | 1106 +++++----- .../src/core/contexts/FilesModalContext.tsx | 363 ++- frontend/src/core/contexts/HotkeyContext.tsx | 121 +- .../src/core/contexts/IndexedDBContext.tsx | 73 +- .../src/core/contexts/NavigationContext.tsx | 300 +-- .../src/core/contexts/PageEditorContext.tsx | 287 +-- .../src/core/contexts/PreferencesContext.tsx | 28 +- .../src/core/contexts/RedactionContext.tsx | 41 +- .../src/core/contexts/RightRailContext.tsx | 148 +- .../src/core/contexts/SaaSTeamContext.tsx | 2 +- frontend/src/core/contexts/SidebarContext.tsx | 61 +- .../src/core/contexts/SignatureContext.tsx | 22 +- .../src/core/contexts/ToolActionsContext.tsx | 2 +- .../src/core/contexts/ToolRegistryContext.tsx | 8 +- .../core/contexts/ToolRegistryProvider.tsx | 16 +- .../src/core/contexts/ToolWorkflowContext.tsx | 515 ++--- .../contexts/TourOrchestrationContext.tsx | 59 +- .../core/contexts/UnsavedChangesContext.tsx | 17 +- frontend/src/core/contexts/ViewerContext.tsx | 138 +- .../src/core/contexts/file/FileReducer.ts | 135 +- frontend/src/core/contexts/file/contexts.ts | 6 +- .../src/core/contexts/file/fileActions.ts | 484 ++-- frontend/src/core/contexts/file/fileHooks.ts | 205 +- .../src/core/contexts/file/fileSelectors.ts | 41 +- frontend/src/core/contexts/file/lifecycle.ts | 35 +- .../toolWorkflow/toolWorkflowState.ts | 47 +- .../src/core/contexts/viewer/viewerActions.ts | 54 +- .../src/core/contexts/viewer/viewerBridges.ts | 18 +- frontend/src/core/data/toolsTaxonomy.ts | 154 +- .../core/data/useProprietaryToolRegistry.tsx | 2 +- .../core/data/useTranslatedToolRegistry.tsx | 132 +- frontend/src/core/env.test.ts | 36 +- frontend/src/core/extensions/accountLogout.ts | 4 +- .../core/hooks/signing/useSigningSessions.ts | 28 +- .../core/hooks/signing/useSigningWorkbench.ts | 29 +- .../useAddAttachmentsOperation.ts | 16 +- .../useAddAttachmentsParameters.ts | 13 +- .../useAddPasswordOperation.test.ts | 96 +- .../addPassword/useAddPasswordOperation.ts | 18 +- .../useAddPasswordParameters.test.ts | 148 +- .../addPassword/useAddPasswordParameters.ts | 16 +- .../addWatermark/useAddWatermarkOperation.ts | 20 +- .../addWatermark/useAddWatermarkParameters.ts | 19 +- .../useAdjustContrastOperation.ts | 195 +- .../useAdjustContrastParameters.ts | 6 +- .../useAdjustPageScaleOperation.ts | 16 +- .../useAdjustPageScaleParameters.test.ts | 91 +- .../useAdjustPageScaleParameters.ts | 26 +- .../autoRename/useAutoRenameOperation.ts | 23 +- .../autoRename/useAutoRenameParameters.ts | 8 +- .../tools/automate/useAutomateOperation.ts | 75 +- .../hooks/tools/automate/useAutomationForm.ts | 77 +- .../tools/automate/useSavedAutomations.ts | 106 +- .../tools/automate/useSuggestedAutomations.ts | 177 +- .../useBookletImpositionOperation.ts | 21 +- .../useBookletImpositionParameters.ts | 16 +- .../tools/certSign/useCertSignOperation.ts | 52 +- .../tools/certSign/useCertSignParameters.ts | 40 +- .../useChangeMetadataOperation.test.ts | 146 +- .../useChangeMetadataOperation.ts | 30 +- .../useChangeMetadataParameters.test.ts | 120 +- .../useChangeMetadataParameters.ts | 67 +- .../changeMetadata/useMetadataExtraction.ts | 22 +- .../useChangePermissionsOperation.test.ts | 61 +- .../useChangePermissionsOperation.ts | 25 +- .../useChangePermissionsParameters.test.ts | 50 +- .../useChangePermissionsParameters.ts | 6 +- .../hooks/tools/compare/operationUtils.ts | 102 +- .../tools/compare/useCompareOperation.ts | 227 +- .../tools/compare/useCompareParameters.ts | 6 +- .../tools/compress/useCompressOperation.ts | 18 +- .../tools/compress/useCompressParameters.ts | 18 +- .../tools/convert/useConvertOperation.ts | 195 +- .../convert/useConvertParameters.test.ts | 157 +- .../tools/convert/useConvertParameters.ts | 443 ++-- .../useConvertParametersAutoDetection.test.ts | 245 +-- .../core/hooks/tools/crop/useCropOperation.ts | 16 +- .../hooks/tools/crop/useCropParameters.ts | 161 +- .../useEditTableOfContentsOperation.ts | 23 +- .../useEditTableOfContentsParameters.ts | 24 +- .../useExtractImagesOperation.ts | 33 +- .../useExtractImagesParameters.ts | 10 +- .../extractPages/useExtractPagesOperation.ts | 34 +- .../extractPages/useExtractPagesParameters.ts | 12 +- .../tools/flatten/useFlattenOperation.ts | 20 +- .../tools/flatten/useFlattenParameters.ts | 8 +- .../getPdfInfo/useGetPdfInfoOperation.ts | 63 +- .../getPdfInfo/useGetPdfInfoParameters.ts | 8 +- .../tools/merge/useMergeOperation.test.ts | 76 +- .../hooks/tools/merge/useMergeOperation.ts | 18 +- .../tools/merge/useMergeParameters.test.ts | 32 +- .../hooks/tools/merge/useMergeParameters.ts | 6 +- .../core/hooks/tools/ocr/useOCROperation.ts | 107 +- .../core/hooks/tools/ocr/useOCRParameters.ts | 10 +- .../overlayPdfs/useOverlayPdfsOperation.ts | 30 +- .../overlayPdfs/useOverlayPdfsParameters.ts | 20 +- .../pageLayout/usePageLayoutOperation.ts | 48 +- .../pageLayout/usePageLayoutParameters.ts | 36 +- .../tools/redact/useRedactOperation.test.ts | 100 +- .../hooks/tools/redact/useRedactOperation.ts | 24 +- .../tools/redact/useRedactParameters.test.ts | 104 +- .../hooks/tools/redact/useRedactParameters.ts | 22 +- .../useRemoveAnnotationsOperation.ts | 38 +- .../useRemoveAnnotationsParameters.ts | 11 +- .../removeBlanks/useRemoveBlanksOperation.ts | 39 +- .../removeBlanks/useRemoveBlanksParameters.ts | 8 +- .../useRemoveCertificateSignOperation.ts | 19 +- .../useRemoveCertificateSignParameters.ts | 8 +- .../removeImage/useRemoveImageOperation.ts | 20 +- .../removeImage/useRemoveImageParameters.ts | 8 +- .../removePages/useRemovePagesOperation.ts | 22 +- .../removePages/useRemovePagesParameters.ts | 10 +- .../buildRemovePasswordFormData.ts | 2 +- .../useRemovePasswordOperation.test.ts | 73 +- .../useRemovePasswordOperation.ts | 18 +- .../useRemovePasswordParameters.test.ts | 62 +- .../useRemovePasswordParameters.ts | 10 +- .../useReorganizePagesOperation.ts | 26 +- .../useReorganizePagesParameters.ts | 17 +- .../hooks/tools/repair/useRepairOperation.ts | 14 +- .../hooks/tools/repair/useRepairParameters.ts | 8 +- .../replaceColor/useReplaceColorOperation.ts | 30 +- .../replaceColor/useReplaceColorParameters.ts | 20 +- .../tools/rotate/useRotateOperation.test.ts | 85 +- .../hooks/tools/rotate/useRotateOperation.ts | 14 +- .../tools/rotate/useRotateParameters.test.ts | 36 +- .../hooks/tools/rotate/useRotateParameters.ts | 24 +- .../tools/sanitize/useSanitizeOperation.ts | 28 +- .../sanitize/useSanitizeParameters.test.ts | 36 +- .../tools/sanitize/useSanitizeParameters.ts | 8 +- .../useScannerImageSplitOperation.ts | 72 +- .../useScannerImageSplitParameters.ts | 8 +- .../tools/shared/toolOperationHelpers.ts | 19 +- .../hooks/tools/shared/toolOperationTypes.ts | 19 +- .../hooks/tools/shared/useAccordionSteps.ts | 72 +- .../hooks/tools/shared/useBaseParameters.ts | 4 +- .../core/hooks/tools/shared/useBaseTool.ts | 36 +- .../hooks/tools/shared/useOperationResults.ts | 16 +- .../hooks/tools/shared/useToolApiCalls.ts | 165 +- .../hooks/tools/shared/useToolOperation.ts | 754 ++++--- .../hooks/tools/shared/useToolResources.ts | 96 +- .../core/hooks/tools/shared/useToolState.ts | 84 +- .../hooks/tools/shared/useViewScopedFiles.ts | 14 +- .../hooks/tools/showJS/useShowJSOperation.ts | 222 +- .../hooks/tools/showJS/useShowJSParameters.ts | 14 +- .../hooks/tools/sign/useSavedSignatures.ts | 114 +- .../core/hooks/tools/sign/useSignOperation.ts | 38 +- .../hooks/tools/sign/useSignParameters.ts | 26 +- .../useSingleLargePageOperation.ts | 16 +- .../useSingleLargePageParameters.ts | 8 +- .../hooks/tools/split/useSplitOperation.ts | 39 +- .../hooks/tools/split/useSplitParameters.ts | 32 +- .../timestampPdf/useTimestampPdfOperation.ts | 18 +- .../timestampPdf/useTimestampPdfParameters.ts | 16 +- .../useUnlockPdfFormsOperation.ts | 16 +- .../useUnlockPdfFormsParameters.ts | 8 +- .../core/hooks/tools/useFavoriteToolItems.ts | 12 +- .../core/hooks/tools/useToolPanelGeometry.ts | 21 +- .../core/hooks/tools/useUserToolActivity.ts | 19 +- .../CenteredMessageSection.ts | 6 +- .../outputtedPDFSections/FieldBoxSection.ts | 19 +- .../outputtedPDFSections/SignatureSection.ts | 60 +- .../StatusBadgeSection.ts | 2 +- .../outputtedPDFSections/SummarySection.ts | 36 +- .../outputtedPDFSections/ThumbnailSection.ts | 10 +- .../validateSignature/signatureReportPdf.ts | 30 +- .../useValidateSignatureOperation.ts | 87 +- .../useValidateSignatureParameters.ts | 4 +- .../validateSignature/utils/pdfPageHelpers.ts | 22 +- .../validateSignature/utils/pdfPalette.ts | 37 +- .../tools/validateSignature/utils/pdfText.ts | 12 +- .../validateSignature/utils/reportStatus.ts | 18 +- .../validateSignature/utils/signatureCsv.ts | 131 +- .../utils/signatureReportBuilder.ts | 12 +- .../utils/signatureStatus.ts | 48 +- .../validateSignature/utils/signatureUtils.ts | 36 +- frontend/src/core/hooks/useAdminSettings.ts | 27 +- frontend/src/core/hooks/useAuditFilters.ts | 11 +- frontend/src/core/hooks/useBackendHealth.ts | 4 +- frontend/src/core/hooks/useBackendProbe.ts | 38 +- frontend/src/core/hooks/useBaseUrl.ts | 4 +- .../core/hooks/useConversionCloudStatus.ts | 6 +- frontend/src/core/hooks/useCookieConsent.ts | 200 +- frontend/src/core/hooks/useDocumentMeta.ts | 66 +- frontend/src/core/hooks/useEndpointConfig.ts | 58 +- .../core/hooks/useEnhancedProcessedFiles.ts | 46 +- frontend/src/core/hooks/useFileActionIcons.ts | 8 +- .../core/hooks/useFileActionTerminology.ts | 26 +- frontend/src/core/hooks/useFileHandler.ts | 23 +- frontend/src/core/hooks/useFileManager.ts | 494 +++-- frontend/src/core/hooks/useFileWithUrl.ts | 8 +- frontend/src/core/hooks/useFocusTrap.ts | 25 +- frontend/src/core/hooks/useFooterInfo.ts | 8 +- .../src/core/hooks/useGoogleDrivePicker.ts | 18 +- frontend/src/core/hooks/useGroupEnabled.ts | 18 +- .../src/core/hooks/useGroupSigningEnabled.ts | 2 +- .../src/core/hooks/useIndexedDBThumbnail.ts | 15 +- frontend/src/core/hooks/useIsMobile.ts | 6 +- frontend/src/core/hooks/useIsOverflowing.ts | 16 +- frontend/src/core/hooks/useJwtConfigSync.ts | 16 +- frontend/src/core/hooks/useLicenseAlert.ts | 17 +- frontend/src/core/hooks/useLoginRequired.ts | 65 +- frontend/src/core/hooks/useLogoAssets.test.ts | 52 +- frontend/src/core/hooks/useLogoAssets.ts | 13 +- frontend/src/core/hooks/useLogoPath.ts | 8 +- frontend/src/core/hooks/useLogoVariant.ts | 11 +- frontend/src/core/hooks/useOs.ts | 64 +- frontend/src/core/hooks/usePDFProcessor.ts | 137 +- frontend/src/core/hooks/usePdfLibLinks.ts | 22 +- .../core/hooks/usePdfSignatureDetection.ts | 17 +- frontend/src/core/hooks/useProcessedFiles.ts | 36 +- .../core/hooks/useProgressivePagePreviews.ts | 252 ++- frontend/src/core/hooks/useRainbowTheme.ts | 63 +- .../src/core/hooks/useRightRailButtons.ts | 21 +- .../src/core/hooks/useRightRailTooltipSide.ts | 24 +- frontend/src/core/hooks/useScarfTracking.ts | 20 +- .../hooks/useSelfHostedToolAvailability.ts | 4 +- .../src/core/hooks/useServerExperience.ts | 138 +- frontend/src/core/hooks/useSettingsDirty.ts | 8 +- frontend/src/core/hooks/useSharingEnabled.ts | 2 +- .../core/hooks/useShouldShowWelcomeModal.ts | 14 +- .../src/core/hooks/useSidebarNavigation.ts | 33 +- frontend/src/core/hooks/useSuggestedTools.ts | 66 +- .../src/core/hooks/useThumbnailGeneration.ts | 70 +- frontend/src/core/hooks/useToolManagement.tsx | 140 +- frontend/src/core/hooks/useToolNavigation.ts | 49 +- frontend/src/core/hooks/useToolParameters.ts | 20 +- frontend/src/core/hooks/useToolSections.ts | 34 +- frontend/src/core/hooks/useTooltipPosition.ts | 45 +- frontend/src/core/hooks/useTranslation.ts | 22 +- frontend/src/core/hooks/useUndoRedo.ts | 24 +- frontend/src/core/hooks/useUrlSync.ts | 82 +- .../src/core/hooks/useViewerKeyCommand.ts | 4 +- frontend/src/core/hooks/useWheelZoom.ts | 6 +- frontend/src/core/hooks/useZipConfirmation.ts | 8 +- frontend/src/core/i18n.ts | 153 +- frontend/src/core/i18n/config.ts | 70 +- frontend/src/core/i18n/tomlBackend.ts | 17 +- frontend/src/core/pages/HomePage.css | 4 +- frontend/src/core/pages/HomePage.tsx | 111 +- frontend/src/core/pages/MobileScannerPage.tsx | 542 ++--- frontend/src/core/services/accountService.ts | 36 +- frontend/src/core/services/apiClient.ts | 13 +- frontend/src/core/services/apiClientConfig.ts | 2 +- frontend/src/core/services/apiClientSetup.ts | 8 +- frontend/src/core/services/auditService.ts | 29 +- .../src/core/services/automationStorage.ts | 63 +- .../services/documentManipulationService.ts | 55 +- frontend/src/core/services/downloadService.ts | 6 +- .../services/enhancedPDFProcessingService.ts | 131 +- frontend/src/core/services/errorUtils.ts | 20 +- frontend/src/core/services/fileAnalyzer.ts | 66 +- .../src/core/services/fileDialogService.ts | 4 +- .../core/services/fileProcessingService.ts | 35 +- frontend/src/core/services/fileStorage.ts | 75 +- frontend/src/core/services/fileStubHelpers.ts | 14 +- .../core/services/googleDrivePickerService.ts | 61 +- .../src/core/services/httpErrorHandler.ts | 46 +- frontend/src/core/services/httpErrorUtils.ts | 50 +- .../src/core/services/indexedDBManager.ts | 77 +- .../src/core/services/localFileSaveService.ts | 12 +- .../src/core/services/openFilesFromDisk.ts | 8 +- .../services/operationResultsSaveService.ts | 8 +- .../src/core/services/pdfExportHelpers.ts | 23 +- .../src/core/services/pdfExportService.ts | 100 +- .../src/core/services/pdfMetadataService.ts | 69 +- .../src/core/services/pdfProcessingService.ts | 44 +- .../src/core/services/pdfWorkerManager.ts | 55 +- .../src/core/services/pdfiumDocBuilder.ts | 65 +- frontend/src/core/services/pdfiumService.ts | 429 ++-- .../src/core/services/preferencesService.ts | 37 +- frontend/src/core/services/processingCache.ts | 55 +- .../core/services/processingErrorHandler.ts | 188 +- .../src/core/services/serverStorageBundle.ts | 42 +- .../src/core/services/serverStorageUpload.ts | 62 +- .../src/core/services/shareBundleUtils.ts | 39 +- .../services/signatureDetectionService.ts | 54 +- .../core/services/signatureStorageService.ts | 46 +- .../src/core/services/specialErrorToasts.ts | 32 +- frontend/src/core/services/supabaseClient.ts | 10 +- .../services/thumbnailGenerationService.ts | 48 +- frontend/src/core/services/updateService.ts | 80 +- .../core/services/usageAnalyticsService.ts | 21 +- frontend/src/core/services/zipFileService.ts | 240 +- frontend/src/core/setupTests.js | 2 +- frontend/src/core/setupTests.ts | 30 +- frontend/src/core/styles/cookieconsent.css | 2 +- frontend/src/core/styles/index.css | 20 +- frontend/src/core/styles/rainbow.module.css | 217 +- frontend/src/core/styles/tailwind.css | 3 +- frontend/src/core/styles/theme.css | 444 ++-- frontend/src/core/styles/zIndex.ts | 2 - .../testing/serverExperienceSimulations.ts | 39 +- .../CertificateValidationE2E.spec.ts | 176 +- .../src/core/tests/convert/ConvertE2E.spec.ts | 132 +- .../tests/convert/ConvertIntegration.test.tsx | 665 +++--- .../ConvertSmartDetectionIntegration.test.tsx | 394 ++-- .../core/tests/missingTranslations.test.ts | 78 +- .../src/core/tests/test-fixtures/sample.htm | 232 +- .../src/core/tests/test-fixtures/sample.html | 182 +- frontend/src/core/tests/translation.test.ts | 33 +- .../core/tests/translationStructure.test.ts | 37 +- .../src/core/tests/utils/testFileHelpers.ts | 16 +- frontend/src/core/theme/mantineTheme.ts | 328 +-- frontend/src/core/tools/AddAttachments.tsx | 14 +- frontend/src/core/tools/AddImage.tsx | 12 +- frontend/src/core/tools/AddPageNumbers.tsx | 18 +- frontend/src/core/tools/AddPassword.tsx | 2 - frontend/src/core/tools/AddStamp.tsx | 51 +- frontend/src/core/tools/AddText.tsx | 12 +- frontend/src/core/tools/AddWatermark.tsx | 1 - frontend/src/core/tools/AdjustContrast.tsx | 66 +- frontend/src/core/tools/AdjustPageScale.tsx | 7 +- frontend/src/core/tools/Annotate.tsx | 332 +-- frontend/src/core/tools/AutoRename.tsx | 11 +- frontend/src/core/tools/Automate.tsx | 94 +- frontend/src/core/tools/BookletImposition.tsx | 9 +- frontend/src/core/tools/CertSign.tsx | 87 +- frontend/src/core/tools/ChangeMetadata.tsx | 27 +- frontend/src/core/tools/ChangePermissions.tsx | 7 +- frontend/src/core/tools/Compare.tsx | 380 ++-- frontend/src/core/tools/Compress.tsx | 8 +- frontend/src/core/tools/Convert.tsx | 12 +- frontend/src/core/tools/Crop.tsx | 14 +- .../src/core/tools/EditTableOfContents.tsx | 183 +- frontend/src/core/tools/ExtractImages.tsx | 9 +- frontend/src/core/tools/ExtractPages.tsx | 9 +- frontend/src/core/tools/Flatten.tsx | 7 +- frontend/src/core/tools/GetPdfInfo.tsx | 89 +- frontend/src/core/tools/Merge.tsx | 87 +- frontend/src/core/tools/OverlayPdfs.tsx | 33 +- frontend/src/core/tools/PageLayout.tsx | 61 +- frontend/src/core/tools/Redact.tsx | 75 +- frontend/src/core/tools/RemoveAnnotations.tsx | 9 +- frontend/src/core/tools/RemoveBlanks.tsx | 9 +- .../src/core/tools/RemoveCertificateSign.tsx | 4 +- frontend/src/core/tools/RemoveImage.tsx | 9 +- frontend/src/core/tools/RemovePages.tsx | 8 +- frontend/src/core/tools/RemovePassword.tsx | 7 +- frontend/src/core/tools/ReorganizePages.tsx | 18 +- frontend/src/core/tools/Repair.tsx | 7 +- frontend/src/core/tools/ReplaceColor.tsx | 9 +- frontend/src/core/tools/Rotate.tsx | 14 +- frontend/src/core/tools/Sanitize.tsx | 7 +- frontend/src/core/tools/ScannerImageSplit.tsx | 9 +- frontend/src/core/tools/ShowJS.tsx | 276 +-- frontend/src/core/tools/Sign.tsx | 12 +- frontend/src/core/tools/SingleLargePage.tsx | 7 +- frontend/src/core/tools/Split.tsx | 15 +- frontend/src/core/tools/TimestampPdf.tsx | 12 +- frontend/src/core/tools/UnlockPdfForms.tsx | 7 +- frontend/src/core/tools/ValidateSignature.tsx | 69 +- .../core/tools/annotate/AnnotationPanel.tsx | 505 +++-- .../tools/annotate/useAnnotationSelection.ts | 125 +- .../tools/annotate/useAnnotationStyleState.ts | 104 +- .../formFill/ButtonAppearanceOverlay.tsx | 69 +- .../src/core/tools/formFill/FieldInput.tsx | 72 +- .../core/tools/formFill/FormFieldOverlay.tsx | 353 +-- .../core/tools/formFill/FormFieldSidebar.tsx | 115 +- .../core/tools/formFill/FormFill.module.css | 2 +- frontend/src/core/tools/formFill/FormFill.tsx | 232 +- .../core/tools/formFill/FormFillContext.tsx | 259 +-- .../src/core/tools/formFill/FormSaveBar.tsx | 56 +- .../src/core/tools/formFill/fieldMeta.tsx | 44 +- frontend/src/core/tools/formFill/formApi.ts | 63 +- frontend/src/core/tools/formFill/index.ts | 22 +- .../formFill/providers/PdfBoxFormProvider.ts | 17 +- .../formFill/providers/PdfiumFormProvider.ts | 149 +- .../core/tools/formFill/providers/index.ts | 6 +- .../core/tools/formFill/providers/types.ts | 8 +- frontend/src/core/tools/formFill/types.ts | 23 +- .../tools/pdfTextEditor/PdfTextEditor.tsx | 912 ++++---- .../core/tools/pdfTextEditor/fontAnalysis.ts | 228 +- .../tools/pdfTextEditor/pdfTextEditorTypes.ts | 6 +- .../tools/pdfTextEditor/pdfTextEditorUtils.ts | 232 +- .../src/core/tools/stamp/createStampTool.tsx | 82 +- frontend/src/core/tsconfig.json | 12 +- frontend/src/core/types/appConfig.ts | 4 +- frontend/src/core/types/automation.ts | 11 +- frontend/src/core/types/backendHealth.ts | 2 +- frontend/src/core/types/compare.ts | 48 +- .../src/core/types/endpointAvailability.ts | 2 +- frontend/src/core/types/file.ts | 2 +- frontend/src/core/types/fileContext.ts | 128 +- frontend/src/core/types/fileIdSafety.d.ts | 4 +- frontend/src/core/types/getPdfInfo.ts | 66 +- frontend/src/core/types/metadata.ts | 6 +- frontend/src/core/types/navigation.ts | 5 +- frontend/src/core/types/navigationActions.ts | 6 +- frontend/src/core/types/pageEditor.ts | 12 +- frontend/src/core/types/parameters.ts | 2 +- frontend/src/core/types/processing.ts | 10 +- frontend/src/core/types/proprietaryToolId.ts | 6 +- frontend/src/core/types/rightRail.ts | 64 +- frontend/src/core/types/sidebar.ts | 8 +- frontend/src/core/types/signature.ts | 10 +- frontend/src/core/types/signingSession.ts | 8 +- frontend/src/core/types/tips.ts | 2 +- frontend/src/core/types/tool.ts | 11 +- frontend/src/core/types/toolId.ts | 161 +- frontend/src/core/types/types.ts | 4 +- frontend/src/core/types/workbench.ts | 8 +- .../src/core/utils/automationConverter.ts | 39 +- frontend/src/core/utils/automationExecutor.ts | 98 +- .../src/core/utils/automationFileProcessor.ts | 64 +- frontend/src/core/utils/browserIdentifier.ts | 10 +- .../bulkselection/parseSelection.test.ts | 242 +- .../utils/bulkselection/parseSelection.ts | 77 +- .../utils/bulkselection/selectionBuilders.ts | 66 +- frontend/src/core/utils/clickHandlers.ts | 8 +- frontend/src/core/utils/convertUtils.test.ts | 485 ++--- frontend/src/core/utils/convertUtils.ts | 54 +- frontend/src/core/utils/cropCoordinates.ts | 66 +- frontend/src/core/utils/downloadUtils.ts | 29 +- .../src/core/utils/editTableOfContents.ts | 14 +- frontend/src/core/utils/fileDialogUtils.ts | 10 +- frontend/src/core/utils/fileHash.ts | 39 +- frontend/src/core/utils/fileHistoryUtils.ts | 14 +- frontend/src/core/utils/fileIdSafety.ts | 4 +- .../src/core/utils/fileResponseUtils.test.ts | 151 +- frontend/src/core/utils/fileResponseUtils.ts | 14 +- frontend/src/core/utils/fileUtils.test.ts | 82 +- frontend/src/core/utils/fileUtils.ts | 78 +- frontend/src/core/utils/fuzzySearch.ts | 34 +- frontend/src/core/utils/genericUtils.ts | 2 +- .../src/core/utils/homePageNavigation.test.ts | 80 +- frontend/src/core/utils/homePageNavigation.ts | 16 +- frontend/src/core/utils/hotkeys.ts | 108 +- frontend/src/core/utils/imageToPdfUtils.ts | 98 +- frontend/src/core/utils/imageTransparency.ts | 48 +- frontend/src/core/utils/languageMapping.ts | 1054 +++++---- frontend/src/core/utils/pageMetadata.ts | 32 +- frontend/src/core/utils/pageSelection.ts | 13 +- frontend/src/core/utils/pdfLinkUtils.ts | 73 +- frontend/src/core/utils/pdfiumBitmapUtils.ts | 49 +- frontend/src/core/utils/resourceManager.ts | 18 +- frontend/src/core/utils/scarfTracking.ts | 11 +- frontend/src/core/utils/scriptLoader.ts | 2 +- frontend/src/core/utils/settingsNavigation.ts | 10 +- .../src/core/utils/settingsPendingHelper.ts | 33 +- frontend/src/core/utils/sidebarUtils.ts | 14 +- .../src/core/utils/signatureFlattening.ts | 112 +- frontend/src/core/utils/signaturePreview.ts | 26 +- frontend/src/core/utils/storageUtils.ts | 36 +- frontend/src/core/utils/textDiff.ts | 18 +- frontend/src/core/utils/textUtils.ts | 9 +- frontend/src/core/utils/thumbnailUtils.ts | 383 +++- frontend/src/core/utils/toolErrorHandler.ts | 47 +- .../src/core/utils/toolResponseProcessor.ts | 22 +- frontend/src/core/utils/toolSearch.ts | 16 +- frontend/src/core/utils/urlMapping.ts | 214 +- frontend/src/core/utils/urlRouting.ts | 42 +- frontend/src/core/utils/viewerZoom.ts | 24 +- frontend/src/core/workers/compareWorker.ts | 94 +- frontend/src/desktop/auth/supabase.ts | 26 +- .../src/desktop/components/AppProviders.tsx | 124 +- .../components/BackendHealthIndicator.tsx | 59 +- .../desktop/components/ConnectionSettings.tsx | 61 +- .../components/DesktopBannerInitializer.tsx | 12 +- .../desktop/components/DesktopConfigSync.tsx | 8 +- .../components/DesktopOnboardingModal.tsx | 88 +- .../components/SaveShortcutListener.tsx | 4 +- .../SetupWizard/DesktopAuthLayout.tsx | 39 +- .../SetupWizard/DesktopOAuthButtons.tsx | 73 +- .../SetupWizard/SaaSLoginScreen.tsx | 54 +- .../SetupWizard/SaaSSignupScreen.tsx | 34 +- .../components/SetupWizard/SelfHostedLink.tsx | 17 +- .../SetupWizard/SelfHostedLoginScreen.tsx | 53 +- .../SetupWizard/ServerSelection.tsx | 141 +- .../SetupWizard/ServerSelectionScreen.tsx | 24 +- .../desktop/components/SetupWizard/index.tsx | 241 +- .../src/desktop/components/SignInModal.tsx | 14 +- .../fileEditor/FileEditorFileName.tsx | 58 +- .../orchestrator/onboardingConfig.ts | 17 +- .../orchestrator/useOnboardingOrchestrator.ts | 6 +- .../QuickAccessBarFooterExtensions.tsx | 40 +- .../rightRail/RightRailFooterExtensions.tsx | 68 +- .../desktop/components/shared/CloudBadge.tsx | 15 +- .../components/shared/DefaultAppBanner.tsx | 12 +- .../shared/DisabledButtonWithTooltip.tsx | 12 +- .../shared/SelfHostedOfflineBanner.tsx | 127 +- .../shared/TeamInvitationBanner.tsx | 53 +- .../shared/billing/SaaSStripeCheckout.tsx | 107 +- .../shared/config/configNavSections.tsx | 91 +- .../configSections/DefaultAppSettings.tsx | 22 +- .../config/configSections/GeneralSection.tsx | 8 +- .../configSections/SaaSTeamsSection.tsx | 238 +- .../config/configSections/SaasPlanSection.tsx | 76 +- .../plan/ActiveSubscriptionCard.tsx | 84 +- .../configSections/plan/PlanUpgradeCard.tsx | 38 +- .../plan/SaaSAvailablePlansSection.tsx | 26 +- .../configSections/plan/SaasPlanCard.tsx | 120 +- .../configSections/plan/UsageDisplay.tsx | 49 +- .../desktop/components/shared/config/types.ts | 10 +- .../shared/modals/CreditExhaustedModal.tsx | 226 +- .../shared/modals/CreditModalBootstrap.tsx | 29 +- .../shared/modals/CreditUsageBanner.tsx | 12 +- .../shared/modals/FeatureListItem.tsx | 22 +- .../modals/InsufficientCreditsModal.tsx | 80 +- .../tools/toolPicker/ToolButton.tsx | 22 +- .../toolPicker/ToolPickerFooterExtensions.tsx | 20 +- .../components/viewer/PrintAPIBridge.tsx | 20 +- frontend/src/desktop/config/billing.ts | 32 +- .../src/desktop/config/defaultAppConfig.ts | 2 +- frontend/src/desktop/config/planFeatures.ts | 74 +- .../src/desktop/constants/backendErrors.ts | 16 +- frontend/src/desktop/constants/connection.ts | 2 +- .../src/desktop/constants/creditEvents.ts | 8 +- .../src/desktop/constants/signInEvents.ts | 2 +- .../desktop/contexts/SaaSCheckoutContext.tsx | 23 +- .../src/desktop/contexts/SaaSTeamContext.tsx | 148 +- .../desktop/contexts/SaasBillingContext.tsx | 134 +- .../src/desktop/extensions/accountLogout.ts | 10 +- .../src/desktop/extensions/authCallback.ts | 6 +- .../desktop/extensions/authSessionCleanup.ts | 14 +- .../desktop/extensions/cookieConsentConfig.ts | 2 +- .../src/desktop/extensions/oauthNavigation.ts | 6 +- .../extensions/platformSessionBridge.ts | 16 +- .../src/desktop/hooks/useAppInitialization.ts | 26 +- .../src/desktop/hooks/useBackendHealth.ts | 29 +- .../desktop/hooks/useBackendInitializer.ts | 12 +- .../desktop/hooks/useConversionCloudStatus.ts | 39 +- frontend/src/desktop/hooks/useCreditCheck.ts | 82 +- frontend/src/desktop/hooks/useCreditEvents.ts | 10 +- frontend/src/desktop/hooks/useDefaultApp.ts | 34 +- .../desktop/hooks/useEnableMeteredBilling.ts | 20 +- .../src/desktop/hooks/useEndpointConfig.ts | 153 +- frontend/src/desktop/hooks/useExitWarning.ts | 59 +- .../src/desktop/hooks/useFileActionIcons.ts | 10 +- .../desktop/hooks/useFileActionTerminology.ts | 26 +- .../src/desktop/hooks/useFirstLaunchCheck.ts | 8 +- frontend/src/desktop/hooks/useGroupEnabled.ts | 20 +- .../desktop/hooks/useGroupSigningEnabled.ts | 4 +- frontend/src/desktop/hooks/useOpenedFile.ts | 18 +- frontend/src/desktop/hooks/useSaaSMode.ts | 8 +- frontend/src/desktop/hooks/useSaaSPlans.ts | 86 +- frontend/src/desktop/hooks/useSaveShortcut.ts | 25 +- .../src/desktop/hooks/useSelfHostedAuth.ts | 14 +- .../hooks/useSelfHostedToolAvailability.ts | 26 +- .../src/desktop/hooks/useSharingEnabled.ts | 6 +- .../src/desktop/hooks/useToolCloudStatus.ts | 14 +- .../src/desktop/hooks/useViewerKeyCommand.ts | 29 +- frontend/src/desktop/hooks/useWillUseCloud.ts | 10 +- frontend/src/desktop/routes/Landing.tsx | 2 +- frontend/src/desktop/routes/Login.tsx | 2 +- .../src/desktop/routes/login/LoginHeader.tsx | 20 +- frontend/src/desktop/services/apiClient.ts | 14 +- .../src/desktop/services/apiClientConfig.ts | 6 +- .../src/desktop/services/apiClientSetup.ts | 72 +- frontend/src/desktop/services/authService.ts | 513 +++-- .../src/desktop/services/authTokenStore.ts | 10 +- .../desktop/services/backendHealthMonitor.ts | 37 +- .../desktop/services/backendReadinessGuard.ts | 31 +- .../desktop/services/connectionModeService.ts | 325 +-- .../src/desktop/services/defaultAppService.ts | 24 +- .../services/desktopNotificationService.ts | 47 +- .../src/desktop/services/downloadService.ts | 6 +- .../services/endpointAvailabilityService.ts | 63 +- .../src/desktop/services/fileDialogService.ts | 28 +- .../src/desktop/services/fileOpenService.ts | 46 +- .../src/desktop/services/httpErrorHandler.ts | 2 +- .../desktop/services/localFileSaveService.ts | 30 +- .../desktop/services/nativePrintService.ts | 5 +- .../services/operationResultsSaveService.ts | 12 +- .../src/desktop/services/operationRouter.ts | 128 +- .../desktop/services/saasBillingService.ts | 141 +- .../desktop/services/saasErrorInterceptor.ts | 10 +- .../services/selfHostedServerMonitor.ts | 26 +- .../desktop/services/tauriBackendService.ts | 96 +- .../src/desktop/services/tauriHttpClient.ts | 180 +- frontend/src/desktop/tsconfig.json | 23 +- frontend/src/desktop/types/billing.ts | 20 +- .../src/desktop/utils/oauthCallbackHtml.ts | 24 +- frontend/src/global.d.ts | 8 +- frontend/src/index.tsx | 44 +- frontend/src/proprietary/App.tsx | 4 +- frontend/src/proprietary/auth/UseSession.tsx | 125 +- frontend/src/proprietary/auth/oauthStorage.ts | 12 +- frontend/src/proprietary/auth/oauthTypes.ts | 18 +- .../proprietary/auth/springAuthClient.test.ts | 222 +- .../src/proprietary/auth/springAuthClient.ts | 248 ++- .../proprietary/components/AppProviders.tsx | 5 +- .../shared/ChangeUserPasswordModal.tsx | 141 +- .../components/shared/DividerWithText.tsx | 41 +- .../components/shared/InviteMembersModal.tsx | 338 +-- .../components/shared/LoginRightCarousel.tsx | 124 +- .../components/shared/ManageBillingButton.tsx | 33 +- .../components/shared/UpdateSeatsButton.tsx | 25 +- .../components/shared/UpdateSeatsModal.tsx | 89 +- .../components/shared/UpgradeBanner.tsx | 157 +- .../shared/UpgradeBannerInitializer.tsx | 7 +- .../config/EnterpriseRequiredBanner.tsx | 18 +- .../shared/config/OverviewHeader.tsx | 20 +- .../shared/config/configNavSections.tsx | 356 +-- .../config/configSections/AccountSection.tsx | 275 +-- .../configSections/AdminAdvancedSection.tsx | 1937 +++++++++-------- .../configSections/AdminAuditSection.tsx | 102 +- .../AdminConnectionsSection.tsx | 636 +++--- .../configSections/AdminDatabaseSection.tsx | 843 ++++--- .../configSections/AdminEndpointsSection.tsx | 481 ++-- .../configSections/AdminFeaturesSection.tsx | 346 +-- .../configSections/AdminGeneralSection.tsx | 1381 ++++++------ .../configSections/AdminLegalSection.tsx | 265 ++- .../configSections/AdminMailSection.tsx | 321 +-- .../configSections/AdminPlanSection.tsx | 101 +- .../configSections/AdminPremiumSection.tsx | 209 +- .../configSections/AdminPrivacySection.tsx | 302 +-- .../configSections/AdminSecuritySection.tsx | 1722 ++++++++------- .../AdminStorageSharingSection.tsx | 480 ++-- .../configSections/AdminUsageSection.tsx | 174 +- .../shared/config/configSections/ApiKeys.tsx | 48 +- .../config/configSections/PeopleSection.tsx | 478 ++-- .../configSections/TeamDetailsSection.tsx | 392 ++-- .../config/configSections/TeamsSection.tsx | 355 +-- .../configSections/apiKeys/ApiKeySection.tsx | 56 +- .../configSections/apiKeys/RefreshModal.tsx | 26 +- .../configSections/apiKeys/hooks/useApiKey.ts | 54 +- .../audit/AuditChartsSection.tsx | 119 +- .../audit/AuditClearDataSection.tsx | 95 +- .../configSections/audit/AuditEventsTable.tsx | 407 ++-- .../audit/AuditExportSection.tsx | 165 +- .../configSections/audit/AuditFiltersForm.tsx | 76 +- .../configSections/audit/AuditStatsCards.tsx | 77 +- .../audit/AuditSystemStatus.tsx | 52 +- .../plan/AvailablePlansSection.tsx | 44 +- .../plan/FeatureComparisonTable.tsx | 53 +- .../configSections/plan/LicenseKeySection.tsx | 150 +- .../config/configSections/plan/PlanCard.tsx | 165 +- .../plan/StaticCheckoutModal.tsx | 208 +- .../configSections/plan/StaticPlanSection.tsx | 207 +- .../usage/UsageAnalyticsChart.tsx | 32 +- .../usage/UsageAnalyticsTable.tsx | 46 +- .../dividerWithText/DividerWithText.css | 1 - .../components/shared/loginSlides.ts | 38 +- .../shared/stripeCheckout/StripeCheckout.tsx | 126 +- .../components/PriceDisplay.tsx | 34 +- .../components/PricingBadge.tsx | 14 +- .../hooks/useCheckoutNavigation.ts | 27 +- .../hooks/useCheckoutSession.ts | 64 +- .../stripeCheckout/hooks/useCheckoutState.ts | 36 +- .../stripeCheckout/hooks/useLicensePolling.ts | 53 +- .../components/shared/stripeCheckout/index.ts | 6 +- .../stripeCheckout/stages/EmailStage.tsx | 31 +- .../stripeCheckout/stages/ErrorStage.tsx | 10 +- .../stripeCheckout/stages/PaymentStage.tsx | 27 +- .../stages/PlanSelectionStage.tsx | 90 +- .../stripeCheckout/stages/SuccessStage.tsx | 68 +- .../shared/stripeCheckout/types/checkout.ts | 8 +- .../shared/stripeCheckout/utils/cardStyles.ts | 22 +- .../stripeCheckout/utils/checkoutUtils.ts | 36 +- .../stripeCheckout/utils/pricingUtils.ts | 10 +- .../stripeCheckout/utils/savingsCalculator.ts | 17 +- .../components/workflow/ParticipantView.tsx | 158 +- .../proprietary/constants/planConstants.ts | 152 +- .../constants/staticStripeLinks.ts | 10 +- .../proprietary/contexts/CheckoutContext.tsx | 200 +- .../proprietary/contexts/LicenseContext.tsx | 44 +- .../contexts/ServerExperienceContext.tsx | 141 +- .../contexts/UpdateSeatsContext.tsx | 169 +- .../proprietary/extensions/accountLogout.ts | 4 +- frontend/src/proprietary/hooks/usePlans.ts | 13 +- .../proprietary/hooks/useRequestHeaders.ts | 4 +- .../proprietary/hooks/useServerExperience.ts | 3 +- .../hooks/useShouldShowWelcomeModal.ts | 12 +- .../hooks/workflow/useParticipantSession.ts | 74 +- .../routes/AuthCallback.module.css | 2 +- .../proprietary/routes/AuthCallback.test.tsx | 100 +- .../src/proprietary/routes/AuthCallback.tsx | 48 +- .../src/proprietary/routes/InviteAccept.tsx | 106 +- frontend/src/proprietary/routes/Landing.tsx | 82 +- .../src/proprietary/routes/Login.test.tsx | 389 ++-- frontend/src/proprietary/routes/Login.tsx | 281 +-- .../proprietary/routes/ShareLinkLoader.tsx | 102 +- .../src/proprietary/routes/ShareLinkPage.tsx | 167 +- frontend/src/proprietary/routes/Signup.tsx | 70 +- .../routes/authShared/AuthLayout.tsx | 45 +- .../proprietary/routes/authShared/auth.css | 16 +- .../routes/login/EmailPasswordForm.tsx | 76 +- .../proprietary/routes/login/ErrorMessage.tsx | 2 +- .../routes/login/LoggedInState.tsx | 72 +- .../proprietary/routes/login/LoginHeader.tsx | 15 +- .../routes/login/NavigationLink.tsx | 15 +- .../routes/login/OAuthButtons.test.tsx | 248 +-- .../proprietary/routes/login/OAuthButtons.tsx | 122 +- .../proprietary/routes/signup/AuthService.ts | 34 +- .../proprietary/routes/signup/SignupForm.tsx | 82 +- .../routes/signup/SignupFormValidation.ts | 38 +- .../proprietary/services/apiClientSetup.ts | 68 +- .../proprietary/services/licenseService.ts | 240 +- .../proprietary/services/shareLinkImport.ts | 47 +- .../src/proprietary/services/teamService.ts | 34 +- .../services/userManagementService.ts | 91 +- .../proprietary/services/workflowService.ts | 46 +- .../src/proprietary/styles/auth-theme.css | 4 +- .../testing/serverExperienceSimulations.ts | 44 +- frontend/src/proprietary/tsconfig.json | 18 +- frontend/src/proprietary/types/license.ts | 2 +- .../proprietary/types/proprietaryToolId.ts | 15 +- frontend/src/proprietary/utils/creditCosts.ts | 2 +- .../proprietary/utils/currencyDetection.ts | 102 +- .../proprietary/utils/licenseCheckoutUtils.ts | 84 +- .../src/proprietary/utils/planTierUtils.ts | 15 +- .../proprietary/utils/protocolDetection.ts | 12 +- frontend/src/proprietary/utils/urlMapping.ts | 4 +- frontend/src/prototypes/App.tsx | 4 +- .../prototypes/components/AppProviders.tsx | 9 +- .../components/chat/ChatContext.tsx | 107 +- .../prototypes/components/chat/ChatPanel.tsx | 18 +- frontend/src/prototypes/tsconfig.json | 23 +- frontend/src/reportWebVitals.js | 4 +- frontend/src/saas/App.tsx | 40 +- frontend/src/saas/auth/UseSession.tsx | 833 +++---- frontend/src/saas/auth/supabase.ts | 153 +- .../saas/components/OnboardingBootstrap.tsx | 36 +- .../saas/components/TrialExpiredBootstrap.tsx | 40 +- .../saas/components/auth/GuestUserBanner.css | 6 +- .../saas/components/auth/GuestUserBanner.tsx | 82 +- .../src/saas/components/auth/RequireAuth.tsx | 32 +- .../components/feedback/UserbackWidget.tsx | 15 +- .../components/home/HomePageExtensions.tsx | 2 +- .../onboarding/SaasOnboardingModal.tsx | 74 +- .../components/onboarding/renderButtons.tsx | 46 +- .../components/onboarding/saasFlowResolver.ts | 19 +- .../onboarding/saasOnboardingFlowConfig.ts | 120 +- .../onboarding/slides/FreeTrialSlide.tsx | 74 +- .../onboarding/useSaasOnboardingState.ts | 68 +- .../saas/components/shared/AppConfigModal.tsx | 248 ++- .../src/saas/components/shared/InfoBanner.tsx | 76 +- .../components/shared/ManageBillingButton.tsx | 29 +- .../saas/components/shared/PrivateContent.tsx | 13 +- .../components/shared/StripeCheckoutSaas.tsx | 97 +- .../components/shared/TrialExpiredModal.tsx | 107 +- .../components/shared/TrialStatusBanner.tsx | 46 +- .../shared/charts/StackedBarChart.tsx | 284 +-- .../stackedBarChart/StackedBarTooltip.tsx | 34 +- .../components/shared/charts/utils/d3Utils.ts | 65 +- .../shared/charts/utils/themeUtils.ts | 39 +- .../shared/charts/utils/tooltipUtils.ts | 35 +- .../shared/config/ProfilePictureCropper.tsx | 73 +- .../shared/config/configSections/ApiKeys.tsx | 92 +- .../shared/config/configSections/Overview.tsx | 292 +-- .../configSections/PasswordSecurity.tsx | 80 +- .../shared/config/configSections/Plan.tsx | 153 +- .../configSections/apiKeys/UsageSection.tsx | 74 +- .../configSections/apiKeys/hooks/useApiKey.ts | 2 - .../apiKeys/hooks/useCredits.ts | 43 +- .../configSections/plan/ActivePlanSection.tsx | 51 +- .../plan/ApiPackagesSection.tsx | 37 +- .../plan/AvailablePlansSection.tsx | 72 +- .../config/configSections/plan/PlanCard.tsx | 37 +- .../shared/config/saasConfigNavSections.tsx | 87 +- .../saas/components/shared/config/types.ts | 118 +- .../src/saas/components/shared/utils/date.ts | 2 - .../components/tools/sign/SignSettings.tsx | 629 +++--- frontend/src/saas/constants/app.ts | 2 +- frontend/src/saas/constants/authProviders.ts | 18 +- .../src/saas/hooks/useAutoAnonymousAuth.ts | 137 +- .../src/saas/hooks/useConfigButtonIcon.tsx | 4 +- frontend/src/saas/hooks/useCreditCheck.ts | 65 +- frontend/src/saas/hooks/useCredits.ts | 27 +- frontend/src/saas/hooks/useEndpointConfig.ts | 58 +- frontend/src/saas/hooks/usePlans.ts | 248 +-- frontend/src/saas/routes/AuthCallback.tsx | 191 +- frontend/src/saas/routes/Landing.tsx | 55 +- frontend/src/saas/routes/Login.tsx | 256 +-- frontend/src/saas/routes/ResetPassword.tsx | 221 +- frontend/src/saas/routes/Signup.tsx | 214 +- .../src/saas/routes/authShared/AuthLayout.tsx | 51 +- .../routes/authShared/GuestSignInButton.tsx | 16 +- .../src/saas/routes/authShared/saas-auth.css | 4 +- .../saas/routes/login/EmailPasswordForm.tsx | 60 +- .../src/saas/routes/login/LoadingState.tsx | 28 +- .../src/saas/routes/login/MagicLinkForm.tsx | 44 +- .../src/saas/routes/login/OAuthButtons.tsx | 95 +- .../src/saas/routes/login/SuccessMessage.tsx | 6 +- .../src/saas/routes/signup/AuthService.ts | 51 +- frontend/src/saas/services/accountDeletion.ts | 6 +- frontend/src/saas/services/apiClient.test.ts | 82 +- frontend/src/saas/services/apiClient.ts | 144 +- .../src/saas/services/avatarSyncService.ts | 272 ++- .../saas/services/signatureStorageService.ts | 24 +- .../saas/services/userManagementService.ts | 80 +- frontend/src/saas/services/userService.ts | 18 +- frontend/src/saas/setupTests.ts | 119 +- frontend/src/saas/styles/saas-theme.css | 84 +- frontend/src/saas/styles/zIndex.ts | 5 +- frontend/src/saas/tsconfig.json | 22 +- frontend/src/saas/types/charts.ts | 4 +- frontend/src/saas/types/credits.ts | 6 +- frontend/src/saas/types/stripe.ts | 16 +- frontend/src/saas/utils/appSettings.ts | 10 +- frontend/src/saas/utils/cropImage.ts | 21 +- frontend/src/saas/utils/pathUtils.ts | 32 +- frontend/tailwind.config.js | 107 +- frontend/tsconfig.core.vite.json | 10 +- frontend/tsconfig.desktop.vite.json | 6 +- frontend/tsconfig.json | 34 +- frontend/tsconfig.proprietary.vite.json | 12 +- frontend/tsconfig.prototypes.vite.json | 6 +- frontend/tsconfig.saas.vite.json | 6 +- frontend/vite.config.ts | 138 +- frontend/vitest.config.ts | 108 +- 1359 files changed, 57785 insertions(+), 57461 deletions(-) create mode 100644 frontend/.prettierignore diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e431fd5f7f..643ef13fd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: permissions: actions: read security-events: write + pull-requests: write strategy: fail-fast: false matrix: @@ -84,6 +85,60 @@ jobs: gradle-version: 9.3.1 cache-disabled: true + - name: Check Java formatting (Spotless) + if: matrix.jdk-version == 25 && matrix.spring-security == false + id: spotless-check + run: ./gradlew spotlessCheck + continue-on-error: true + env: + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} + + - name: Comment on Java formatting failure + if: steps.spotless-check.outcome == 'failure' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const marker = ''; + const body = [ + marker, + '### Java Formatting Check Failed', + '', + 'Your code has formatting issues. Run the following command to fix them:', + '', + '```bash', + './gradlew spotlessApply', + '```', + '', + 'Then commit and push the changes.', + ].join('\n'); + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Fail if Java formatting issues found + if: steps.spotless-check.outcome == 'failure' + run: exit 1 + - name: Build with Gradle and spring security ${{ matrix.spring-security }} run: ./gradlew build -PnoSpotless env: @@ -187,6 +242,9 @@ jobs: if: needs.files-changed.outputs.frontend == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: Harden Runner uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -202,6 +260,52 @@ jobs: cache-dependency-path: frontend/package-lock.json - name: Install frontend dependencies run: cd frontend && npm ci + - name: Check TypeScript formatting (Prettier) + id: prettier-check + run: cd frontend && npm run format:check + continue-on-error: true + - name: Comment on TypeScript formatting failure + if: steps.prettier-check.outcome == 'failure' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const marker = ''; + const body = [ + marker, + '### TypeScript Formatting Check Failed', + '', + 'Your code has formatting issues. Run the following command to fix them:', + '', + '```bash', + 'cd frontend && npm run fix', + '```', + '', + 'Then commit and push the changes.', + ].join('\n'); + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + - name: Fail if TypeScript formatting issues found + if: steps.prettier-check.outcome == 'failure' + run: exit 1 - name: Type-check frontend run: cd frontend && npm run prep && npm run typecheck:all - name: Lint frontend diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index 764f748d44..9e4d5d574f 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -2,7 +2,7 @@ name: Pre-commit on: workflow_dispatch: - push: + pull_request: branches: - main @@ -16,9 +16,6 @@ jobs: # Prevents sdist builds → no tar extraction PIP_ONLY_BINARY: ":all:" PIP_DISABLE_PIP_VERSION_CHECK: "1" - permissions: - contents: write - pull-requests: write steps: - name: Harden Runner uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -31,13 +28,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: @@ -57,47 +47,4 @@ jobs: pre-commit run gitleaks --all-files -c .pre-commit-config.yaml pre-commit run end-of-file-fixer --all-files -c .pre-commit-config.yaml pre-commit run trailing-whitespace --all-files -c .pre-commit-config.yaml - continue-on-error: true - - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 - with: - gradle-version: 9.3.1 - - - name: Build with Gradle - run: ./gradlew build - env: - MAVEN_USER: ${{ secrets.MAVEN_USER }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} - - - name: git add - run: | - git add . - git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV - - - name: Create Pull Request - if: env.CHANGES_DETECTED == 'true' - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - token: ${{ steps.setup-bot.outputs.token }} - commit-message: ":file_folder: pre-commit" - committer: ${{ steps.setup-bot.outputs.committer }} - author: ${{ steps.setup-bot.outputs.committer }} - signoff: true - branch: pre-commit - title: "🤖 format everything with pre-commit by ${{ steps.setup-bot.outputs.app-slug }}" - body: | - Auto-generated by [create-pull-request][1] with **${{ steps.setup-bot.outputs.app-slug }}** - - [1]: https://github.com/peter-evans/create-pull-request - draft: false - delete-branch: true - labels: github-actions - sign-commits: true + git diff --exit-code diff --git a/build.gradle b/build.gradle index 62e92d9484..0721c37baa 100644 --- a/build.gradle +++ b/build.gradle @@ -110,11 +110,11 @@ tasks.register('syncAppVersion') { [new File(sim1Path), new File(sim2Path)].each { f -> if (f.exists()) { def content = f.getText('UTF-8') - def matcher = (content =~ /(appVersion:\s*')([^']*)(')/) + def matcher = (content =~ /(appVersion:\s*(['"]))(.*?)(\2)/) if (!matcher.find()) { throw new GradleException("Could not locate appVersion in ${f} for synchronization") } - def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(3)}") + def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(4)}") if (content != updatedContent) { f.write(updatedContent, 'UTF-8') } diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000000..ad5dab21a1 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,10 @@ +dist/ +node_modules/ +public/vendor/ +public/pdfjs*/ +public/js/thirdParty/ +public/css/cookieconsent.css +*.min.* +*.md +*.wxs +src/output.css diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 9bbe913d60..de728c2775 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,62 +1,52 @@ // @ts-check -import eslint from '@eslint/js'; -import globals from 'globals'; -import { defineConfig } from 'eslint/config'; -import tseslint from 'typescript-eslint'; +import eslint from "@eslint/js"; +import globals from "globals"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; -const srcGlobs = [ - 'src/**/*.{js,mjs,jsx,ts,tsx}', -]; -const nodeGlobs = [ - 'scripts/**/*.{js,ts,mjs}', - '*.config.{js,ts,mjs}', -]; +const srcGlobs = ["src/**/*.{js,mjs,jsx,ts,tsx}"]; +const nodeGlobs = ["scripts/**/*.{js,ts,mjs}", "*.config.{js,ts,mjs}"]; const baseRestrictedImportPatterns = [ - { regex: '^\\.', message: "Use @app/* imports instead of relative imports." }, - { regex: '^src/', message: "Use @app/* imports instead of absolute src/ imports." }, + { regex: "^\\.", message: "Use @app/* imports instead of relative imports." }, + { regex: "^src/", message: "Use @app/* imports instead of absolute src/ imports." }, ]; export default defineConfig( { // Everything that contains 3rd party code that we don't want to lint - ignores: [ - 'dist', - 'node_modules', - 'public', - 'src-tauri', - ], + ignores: ["dist", "node_modules", "public", "src-tauri"], }, eslint.configs.recommended, tseslint.configs.recommended, { rules: { - 'no-restricted-imports': [ - 'error', + "no-restricted-imports": [ + "error", { patterns: baseRestrictedImportPatterns, }, ], - '@typescript-eslint/no-empty-object-type': [ - 'error', + "@typescript-eslint/no-empty-object-type": [ + "error", { // Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future - allowInterfaces: 'with-single-extends', + allowInterfaces: "with-single-extends", }, ], - '@typescript-eslint/no-explicit-any': 'off', // Temporarily disabled until codebase conformant - '@typescript-eslint/no-require-imports': 'off', // Temporarily disabled until codebase conformant - '@typescript-eslint/no-unused-vars': [ - 'error', + "@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant + "@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant + "@typescript-eslint/no-unused-vars": [ + "error", { - 'args': 'all', // All function args must be used (or explicitly ignored) - 'argsIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'caughtErrors': 'all', // Caught errors must be used (or explicitly ignored) - 'caughtErrorsIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'destructuredArrayIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'varsIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'ignoreRestSiblings': true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky) + args: "all", // All function args must be used (or explicitly ignored) + argsIgnorePattern: "^_", // Allow unused variables beginning with an underscore + caughtErrors: "all", // Caught errors must be used (or explicitly ignored) + caughtErrorsIgnorePattern: "^_", // Allow unused variables beginning with an underscore + destructuredArrayIgnorePattern: "^_", // Allow unused variables beginning with an underscore + varsIgnorePattern: "^_", // Allow unused variables beginning with an underscore + ignoreRestSiblings: true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky) }, ], }, @@ -65,15 +55,15 @@ export default defineConfig( // Use the stub/shadow pattern instead: define a stub in src/core/ and override in src/desktop/. { files: srcGlobs, - ignores: ['src/desktop/**'], + ignores: ["src/desktop/**"], rules: { - 'no-restricted-imports': [ - 'error', + "no-restricted-imports": [ + "error", { patterns: [ ...baseRestrictedImportPatterns, { - regex: '^@tauri-apps/', + regex: "^@tauri-apps/", message: "Tauri APIs are desktop-only. Review frontend/DeveloperGuide.md for structure advice.", }, ], @@ -84,9 +74,9 @@ export default defineConfig( // Folders that have been cleaned up and are now conformant - stricter rules enforced here { files: [ - 'src/proprietary/**/*.{js,mjs,jsx,ts,tsx}', - 'src/saas/**/*.{js,mjs,jsx,ts,tsx}', - 'src/prototypes/**/*.{js,mjs,jsx,ts,tsx}', + "src/proprietary/**/*.{js,mjs,jsx,ts,tsx}", + "src/saas/**/*.{js,mjs,jsx,ts,tsx}", + "src/prototypes/**/*.{js,mjs,jsx,ts,tsx}", ], languageOptions: { parserOptions: { @@ -95,8 +85,8 @@ export default defineConfig( }, }, rules: { - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unnecessary-type-assertion': 'error', + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", }, }, // Config for browser scripts @@ -105,8 +95,8 @@ export default defineConfig( languageOptions: { globals: { ...globals.browser, - } - } + }, + }, }, // Config for node scripts { @@ -114,7 +104,7 @@ export default defineConfig( languageOptions: { globals: { ...globals.node, - } - } + }, + }, }, ); diff --git a/frontend/index.html b/frontend/index.html index c790b8d1ed..465841285d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + @@ -6,10 +6,7 @@ - + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dbf6bbd484..d6a59cbed7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -114,6 +114,7 @@ "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", + "prettier": "^3.8.1", "puppeteer": "^24.25.0", "tsx": "^4.21.0", "typescript": "^5.9.2", @@ -11308,6 +11309,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "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", diff --git a/frontend/package.json b/frontend/package.json index 93051e2dc7..167fd7181e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -89,8 +89,12 @@ "dev:saas": "npm run prep:saas && vite --mode saas", "dev:desktop": "npm run prep:desktop && vite --mode desktop", "dev:prototypes": "npm run prep && vite --mode prototypes", + "fix": "npm run format && npm run lint:fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "lint": "npm run lint:eslint && npm run lint:cycles", "lint:eslint": "eslint --max-warnings=0", + "lint:fix": "eslint --fix", "lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1", "build": "npm run prep && vite build", "build:core": "npm run prep && vite build --mode core", @@ -176,6 +180,7 @@ "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", + "prettier": "^3.8.1", "puppeteer": "^24.25.0", "tsx": "^4.21.0", "typescript": "^5.9.2", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 14eb25f85c..deb6a63977 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,11 +1,11 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './src/core/tests', - testMatch: '**/*.spec.ts', + testDir: "./src/core/tests", + testMatch: "**/*.spec.ts", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -15,34 +15,34 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', + baseURL: "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - viewport: { width: 1920, height: 1080 } + name: "chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ @@ -68,8 +68,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', + command: "npm run dev", + url: "http://localhost:5173", reuseExistingServer: !process.env.CI, }, -}); \ No newline at end of file +}); diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 57e730c99a..7b8895cce8 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,3 @@ module.exports = { - plugins: [ - require('@tailwindcss/postcss'), - require('autoprefixer'), - ], + plugins: [require("@tailwindcss/postcss"), require("autoprefixer")], }; diff --git a/frontend/public/css/cookieconsentCustomisation.css b/frontend/public/css/cookieconsentCustomisation.css index ec360c20be..fd1a8ff355 100644 --- a/frontend/public/css/cookieconsentCustomisation.css +++ b/frontend/public/css/cookieconsentCustomisation.css @@ -1,206 +1,205 @@ /* Light theme variables */ :root { - --cc-bg: #ffffff; - --cc-primary-color: #1c1c1c; - --cc-secondary-color: #666666; + --cc-bg: #ffffff; + --cc-primary-color: #1c1c1c; + --cc-secondary-color: #666666; - --cc-btn-primary-bg: #007BFF; - --cc-btn-primary-color: #ffffff; - --cc-btn-primary-border-color: #007BFF; - --cc-btn-primary-hover-bg: #0056b3; - --cc-btn-primary-hover-color: #ffffff; - --cc-btn-primary-hover-border-color: #0056b3; + --cc-btn-primary-bg: #007bff; + --cc-btn-primary-color: #ffffff; + --cc-btn-primary-border-color: #007bff; + --cc-btn-primary-hover-bg: #0056b3; + --cc-btn-primary-hover-color: #ffffff; + --cc-btn-primary-hover-border-color: #0056b3; - --cc-btn-secondary-bg: #f1f3f4; - --cc-btn-secondary-color: #1c1c1c; - --cc-btn-secondary-border-color: #f1f3f4; - --cc-btn-secondary-hover-bg: #007BFF; - --cc-btn-secondary-hover-color: #ffffff; - --cc-btn-secondary-hover-border-color: #007BFF; + --cc-btn-secondary-bg: #f1f3f4; + --cc-btn-secondary-color: #1c1c1c; + --cc-btn-secondary-border-color: #f1f3f4; + --cc-btn-secondary-hover-bg: #007bff; + --cc-btn-secondary-hover-color: #ffffff; + --cc-btn-secondary-hover-border-color: #007bff; - --cc-separator-border-color: #e0e0e0; + --cc-separator-border-color: #e0e0e0; - --cc-toggle-on-bg: #007BFF; - --cc-toggle-off-bg: #667481; - --cc-toggle-on-knob-bg: #ffffff; - --cc-toggle-off-knob-bg: #ffffff; + --cc-toggle-on-bg: #007bff; + --cc-toggle-off-bg: #667481; + --cc-toggle-on-knob-bg: #ffffff; + --cc-toggle-off-knob-bg: #ffffff; - --cc-toggle-enabled-icon-color: #ffffff; - --cc-toggle-disabled-icon-color: #ffffff; + --cc-toggle-enabled-icon-color: #ffffff; + --cc-toggle-disabled-icon-color: #ffffff; - --cc-toggle-readonly-bg: #f1f3f4; - --cc-toggle-readonly-knob-bg: #79747E; - --cc-toggle-readonly-knob-icon-color: #f1f3f4; + --cc-toggle-readonly-bg: #f1f3f4; + --cc-toggle-readonly-knob-bg: #79747e; + --cc-toggle-readonly-knob-icon-color: #f1f3f4; - --cc-section-category-border: #e0e0e0; + --cc-section-category-border: #e0e0e0; - --cc-cookie-category-block-bg: #f1f3f4; - --cc-cookie-category-block-border: #f1f3f4; - --cc-cookie-category-block-hover-bg: #e9eff4; - --cc-cookie-category-block-hover-border: #e9eff4; - - --cc-cookie-category-expanded-block-bg: #f1f3f4; - --cc-cookie-category-expanded-block-hover-bg: #e9eff4; + --cc-cookie-category-block-bg: #f1f3f4; + --cc-cookie-category-block-border: #f1f3f4; + --cc-cookie-category-block-hover-bg: #e9eff4; + --cc-cookie-category-block-hover-border: #e9eff4; - --cc-footer-bg: #ffffff; - --cc-footer-color: #1c1c1c; - --cc-footer-border-color: #ffffff; + --cc-cookie-category-expanded-block-bg: #f1f3f4; + --cc-cookie-category-expanded-block-hover-bg: #e9eff4; + + --cc-footer-bg: #ffffff; + --cc-footer-color: #1c1c1c; + --cc-footer-border-color: #ffffff; } /* Dark theme variables */ -.cc--darkmode{ - --cc-bg: #2d2d2d; - --cc-primary-color: #e5e5e5; - --cc-secondary-color: #b0b0b0; +.cc--darkmode { + --cc-bg: #2d2d2d; + --cc-primary-color: #e5e5e5; + --cc-secondary-color: #b0b0b0; - --cc-btn-primary-bg: #4dabf7; - --cc-btn-primary-color: #ffffff; - --cc-btn-primary-border-color: #4dabf7; - --cc-btn-primary-hover-bg: #3d3d3d; - --cc-btn-primary-hover-color: #ffffff; - --cc-btn-primary-hover-border-color: #3d3d3d; + --cc-btn-primary-bg: #4dabf7; + --cc-btn-primary-color: #ffffff; + --cc-btn-primary-border-color: #4dabf7; + --cc-btn-primary-hover-bg: #3d3d3d; + --cc-btn-primary-hover-color: #ffffff; + --cc-btn-primary-hover-border-color: #3d3d3d; - --cc-btn-secondary-bg: #3d3d3d; - --cc-btn-secondary-color: #ffffff; - --cc-btn-secondary-border-color: #3d3d3d; - --cc-btn-secondary-hover-bg: #4dabf7; - --cc-btn-secondary-hover-color: #ffffff; - --cc-btn-secondary-hover-border-color: #4dabf7; + --cc-btn-secondary-bg: #3d3d3d; + --cc-btn-secondary-color: #ffffff; + --cc-btn-secondary-border-color: #3d3d3d; + --cc-btn-secondary-hover-bg: #4dabf7; + --cc-btn-secondary-hover-color: #ffffff; + --cc-btn-secondary-hover-border-color: #4dabf7; - --cc-separator-border-color: #555555; + --cc-separator-border-color: #555555; - --cc-toggle-on-bg: #4dabf7; - --cc-toggle-off-bg: #667481; - --cc-toggle-on-knob-bg: #2d2d2d; - --cc-toggle-off-knob-bg: #2d2d2d; + --cc-toggle-on-bg: #4dabf7; + --cc-toggle-off-bg: #667481; + --cc-toggle-on-knob-bg: #2d2d2d; + --cc-toggle-off-knob-bg: #2d2d2d; - --cc-toggle-enabled-icon-color: #2d2d2d; - --cc-toggle-disabled-icon-color: #2d2d2d; + --cc-toggle-enabled-icon-color: #2d2d2d; + --cc-toggle-disabled-icon-color: #2d2d2d; - --cc-toggle-readonly-bg: #555555; - --cc-toggle-readonly-knob-bg: #8e8e8e; - --cc-toggle-readonly-knob-icon-color: #555555; + --cc-toggle-readonly-bg: #555555; + --cc-toggle-readonly-knob-bg: #8e8e8e; + --cc-toggle-readonly-knob-icon-color: #555555; - --cc-section-category-border: #555555; + --cc-section-category-border: #555555; - --cc-cookie-category-block-bg: #3d3d3d; - --cc-cookie-category-block-border: #3d3d3d; - --cc-cookie-category-block-hover-bg: #4d4d4d; - --cc-cookie-category-block-hover-border: #4d4d4d; - - --cc-cookie-category-expanded-block-bg: #3d3d3d; - --cc-cookie-category-expanded-block-hover-bg: #4d4d4d; + --cc-cookie-category-block-bg: #3d3d3d; + --cc-cookie-category-block-border: #3d3d3d; + --cc-cookie-category-block-hover-bg: #4d4d4d; + --cc-cookie-category-block-hover-border: #4d4d4d; - --cc-footer-bg: #2d2d2d; - --cc-footer-color: #e5e5e5; - --cc-footer-border-color: #2d2d2d; + --cc-cookie-category-expanded-block-bg: #3d3d3d; + --cc-cookie-category-expanded-block-hover-bg: #4d4d4d; + + --cc-footer-bg: #2d2d2d; + --cc-footer-color: #e5e5e5; + --cc-footer-border-color: #2d2d2d; } -.cm__body{ - max-width: 90% !important; - flex-direction: row !important; - align-items: center !important; - +.cm__body { + max-width: 90% !important; + flex-direction: row !important; + align-items: center !important; } -.cm__desc{ - max-width: 70rem !important; +.cm__desc { + max-width: 70rem !important; } -.cm__btns{ - flex-direction: row-reverse !important; - gap:10px !important; - padding-top: 3.4rem !important; +.cm__btns { + flex-direction: row-reverse !important; + gap: 10px !important; + padding-top: 3.4rem !important; } @media only screen and (max-width: 1400px) { - .cm__body{ - max-width: 90% !important; - flex-direction: column !important; - align-items: normal !important; - } + .cm__body { + max-width: 90% !important; + flex-direction: column !important; + align-items: normal !important; + } - .cm__btns{ - padding-top: 1rem !important; - } + .cm__btns { + padding-top: 1rem !important; + } } /* Toggle visibility fixes */ #cc-main .section__toggle { - opacity: 0 !important; /* Keep invisible but functional */ + opacity: 0 !important; /* Keep invisible but functional */ } #cc-main .toggle__icon { - display: flex !important; - align-items: center !important; - justify-content: flex-start !important; + display: flex !important; + align-items: center !important; + justify-content: flex-start !important; } #cc-main .toggle__icon-circle { - display: block !important; - position: absolute !important; - transition: transform 0.25s ease !important; + display: block !important; + position: absolute !important; + transition: transform 0.25s ease !important; } #cc-main .toggle__icon-on, #cc-main .toggle__icon-off { - display: flex !important; - align-items: center !important; - justify-content: center !important; - position: absolute !important; - width: 100% !important; - height: 100% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + position: absolute !important; + width: 100% !important; + height: 100% !important; } /* Ensure toggles are visible in both themes */ #cc-main .toggle__icon { - background: var(--cc-toggle-off-bg) !important; - border: 1px solid var(--cc-toggle-off-bg) !important; + background: var(--cc-toggle-off-bg) !important; + border: 1px solid var(--cc-toggle-off-bg) !important; } #cc-main .section__toggle:checked ~ .toggle__icon { - background: var(--cc-toggle-on-bg) !important; - border: 1px solid var(--cc-toggle-on-bg) !important; + background: var(--cc-toggle-on-bg) !important; + border: 1px solid var(--cc-toggle-on-bg) !important; } /* Ensure toggle text is visible */ #cc-main .pm__section-title { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .pm__section-desc { - color: var(--cc-secondary-color) !important; + color: var(--cc-secondary-color) !important; } /* Make sure the modal has proper contrast */ #cc-main .pm { - background: var(--cc-bg) !important; - color: var(--cc-primary-color) !important; + background: var(--cc-bg) !important; + color: var(--cc-primary-color) !important; } /* Lower z-index so cookie banner appears behind onboarding modals */ #cc-main { - z-index: 100 !important; + z-index: 100 !important; } /* Ensure consent modal text is visible in both themes */ #cc-main .cm { - background: var(--cc-bg) !important; - color: var(--cc-primary-color) !important; + background: var(--cc-bg) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__title { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__desc { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__footer { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__footer-links a, #cc-main .cm__link { - color: var(--cc-primary-color) !important; -} \ No newline at end of file + color: var(--cc-primary-color) !important; +} diff --git a/frontend/public/manifest-classic.json b/frontend/public/manifest-classic.json index 9b47da7d05..d6e81e7ddf 100644 --- a/frontend/public/manifest-classic.json +++ b/frontend/public/manifest-classic.json @@ -23,4 +23,3 @@ "theme_color": "#000000", "background_color": "#ffffff" } - diff --git a/frontend/scripts/build-provisioner.mjs b/frontend/scripts/build-provisioner.mjs index 2f974a195f..52240b766e 100644 --- a/frontend/scripts/build-provisioner.mjs +++ b/frontend/scripts/build-provisioner.mjs @@ -1,28 +1,24 @@ -import { execFileSync } from 'node:child_process'; -import { existsSync, mkdirSync, copyFileSync } from 'node:fs'; -import { join, resolve } from 'node:path'; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, copyFileSync } from "node:fs"; +import { join, resolve } from "node:path"; -if (process.platform !== 'win32') { +if (process.platform !== "win32") { process.exit(0); } const frontendDir = process.cwd(); -const tauriDir = resolve(frontendDir, 'src-tauri'); -const provisionerManifest = join(tauriDir, 'provisioner', 'Cargo.toml'); +const tauriDir = resolve(frontendDir, "src-tauri"); +const provisionerManifest = join(tauriDir, "provisioner", "Cargo.toml"); -execFileSync( - 'cargo', - ['build', '--release', '--manifest-path', provisionerManifest], - { stdio: 'inherit' } -); +execFileSync("cargo", ["build", "--release", "--manifest-path", provisionerManifest], { stdio: "inherit" }); -const provisionerExe = join(tauriDir, 'provisioner', 'target', 'release', 'stirling-provisioner.exe'); +const provisionerExe = join(tauriDir, "provisioner", "target", "release", "stirling-provisioner.exe"); if (!existsSync(provisionerExe)) { throw new Error(`Provisioner binary not found at ${provisionerExe}`); } -const wixDir = join(tauriDir, 'windows', 'wix'); +const wixDir = join(tauriDir, "windows", "wix"); mkdirSync(wixDir, { recursive: true }); -const destExe = join(wixDir, 'stirling-provision.exe'); +const destExe = join(wixDir, "stirling-provision.exe"); copyFileSync(provisionerExe, destExe); diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js index 96566341c6..d15c53393d 100644 --- a/frontend/scripts/generate-icons.js +++ b/frontend/scripts/generate-icons.js @@ -1,11 +1,11 @@ #!/usr/bin/env node -const { icons } = require('@iconify-json/material-symbols'); -const fs = require('fs'); -const path = require('path'); +const { icons } = require("@iconify-json/material-symbols"); +const fs = require("fs"); +const path = require("path"); // Check for verbose flag -const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v'); +const isVerbose = process.argv.includes("--verbose") || process.argv.includes("-v"); // Logging functions const info = (message) => console.log(message); @@ -18,12 +18,12 @@ const debug = (message) => { // Function to scan codebase for LocalIcon usage function scanForUsedIcons() { const usedIcons = new Set(); - const srcDir = path.join(__dirname, '..', 'src'); + const srcDir = path.join(__dirname, "..", "src"); - info('🔍 Scanning codebase for LocalIcon usage...'); + info("🔍 Scanning codebase for LocalIcon usage..."); if (!fs.existsSync(srcDir)) { - console.error('❌ Source directory not found:', srcDir); + console.error("❌ Source directory not found:", srcDir); process.exit(1); } @@ -31,19 +31,19 @@ function scanForUsedIcons() { function scanDirectory(dir) { const files = fs.readdirSync(dir); - files.forEach(file => { + files.forEach((file) => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { scanDirectory(filePath); - } else if (file.endsWith('.tsx') || file.endsWith('.ts')) { - const content = fs.readFileSync(filePath, 'utf8'); + } else if (file.endsWith(".tsx") || file.endsWith(".ts")) { + const content = fs.readFileSync(filePath, "utf8"); // Match LocalIcon usage: const localIconMatches = content.match(/]*icon="([^"]+)"/g); if (localIconMatches) { - localIconMatches.forEach(match => { + localIconMatches.forEach((match) => { const iconMatch = match.match(/icon="([^"]+)"/); if (iconMatch) { usedIcons.add(iconMatch[1]); @@ -55,7 +55,7 @@ function scanForUsedIcons() { // Match LocalIcon usage: const localIconSingleQuoteMatches = content.match(/]*icon='([^']+)'/g); if (localIconSingleQuoteMatches) { - localIconSingleQuoteMatches.forEach(match => { + localIconSingleQuoteMatches.forEach((match) => { const iconMatch = match.match(/icon='([^']+)'/); if (iconMatch) { usedIcons.add(iconMatch[1]); @@ -67,7 +67,7 @@ function scanForUsedIcons() { // Match old material-symbols-rounded spans: icon-name const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g); if (spanMatches) { - spanMatches.forEach(match => { + spanMatches.forEach((match) => { const iconMatch = match.match(/>([^<]+)<\/span>/); if (iconMatch && iconMatch[1].trim()) { const iconName = iconMatch[1].trim(); @@ -80,7 +80,7 @@ function scanForUsedIcons() { // Match Icon component usage: const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); if (iconMatches) { - iconMatches.forEach(match => { + iconMatches.forEach((match) => { const iconMatch = match.match(/icon="material-symbols:([^"]+)"/); if (iconMatch) { usedIcons.add(iconMatch[1]); @@ -92,7 +92,7 @@ function scanForUsedIcons() { // Match icon config usage: icon: 'icon-name' or icon: "icon-name" const iconPropertyMatches = content.match(/icon:\s*(['"])([a-z0-9-]+)\1/g); if (iconPropertyMatches) { - iconPropertyMatches.forEach(match => { + iconPropertyMatches.forEach((match) => { const iconMatch = match.match(/icon:\s*(['"])([a-z0-9-]+)\1/); if (iconMatch) { usedIcons.add(iconMatch[2]); @@ -118,18 +118,20 @@ async function main() { const usedIcons = scanForUsedIcons(); // Check if we need to regenerate (compare with existing) - const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json'); + const outputPath = path.join(__dirname, "..", "src", "assets", "material-symbols-icons.json"); let needsRegeneration = true; if (fs.existsSync(outputPath)) { try { - const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + const existingSet = JSON.parse(fs.readFileSync(outputPath, "utf8")); const existingIcons = Object.keys(existingSet.icons || {}).sort(); const currentIcons = [...usedIcons].sort(); if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) { needsRegeneration = false; - info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`); + info( + `✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`, + ); } } catch { // If we can't parse existing file, regenerate @@ -138,34 +140,34 @@ async function main() { } if (!needsRegeneration) { - info('🎉 No regeneration needed!'); + info("🎉 No regeneration needed!"); process.exit(0); } info(`🔍 Extracting ${usedIcons.length} icons from Material Symbols...`); // Dynamic import of ES module - const { getIcons } = await import('@iconify/utils'); + const { getIcons } = await import("@iconify/utils"); // Extract only our used icons from the full set const extractedIcons = getIcons(icons, usedIcons); if (!extractedIcons) { - console.error('❌ Failed to extract icons'); + console.error("❌ Failed to extract icons"); process.exit(1); } // Check for missing icons const extractedIconNames = Object.keys(extractedIcons.icons || {}); - const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon)); + const missingIcons = usedIcons.filter((icon) => !extractedIconNames.includes(icon)); if (missingIcons.length > 0) { - info(`⚠️ Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`); - info('💡 These icons don\'t exist in Material Symbols. Please use available alternatives.'); + info(`⚠️ Missing icons (${missingIcons.length}): ${missingIcons.join(", ")}`); + info("💡 These icons don't exist in Material Symbols. Please use available alternatives."); } // Create output directory - const outputDir = path.join(__dirname, '..', 'src', 'assets'); + const outputDir = path.join(__dirname, "..", "src", "assets"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } @@ -182,7 +184,7 @@ async function main() { // This file is automatically generated by scripts/generate-icons.js // Do not edit manually - changes will be overwritten -export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')}; +export type MaterialSymbolIcon = ${usedIcons.map((icon) => `'${icon}'`).join(" | ")}; export interface IconSet { prefix: string; @@ -196,7 +198,7 @@ declare const iconSet: IconSet; export default iconSet; `; - const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts'); + const typesPath = path.join(outputDir, "material-symbols-icons.d.ts"); fs.writeFileSync(typesPath, typesContent); info(`📝 Generated types: ${typesPath}`); @@ -204,7 +206,7 @@ export default iconSet; } // Run the main function -main().catch(error => { - console.error('❌ Script failed:', error); +main().catch((error) => { + console.error("❌ Script failed:", error); process.exit(1); }); diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js index e4b40c0e42..339e208326 100644 --- a/frontend/scripts/generate-licenses.js +++ b/frontend/scripts/generate-licenses.js @@ -1,11 +1,11 @@ #!/usr/bin/env node -const { execSync } = require('node:child_process'); -const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('node:fs'); -const path = require('node:path'); +const { execSync } = require("node:child_process"); +const { existsSync, mkdirSync, writeFileSync, readFileSync } = require("node:fs"); +const path = require("node:path"); -const { argv } = require('node:process'); -const inputIdx = argv.indexOf('--input'); +const { argv } = require("node:process"); +const inputIdx = argv.indexOf("--input"); const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null; const POSTPROCESS_ONLY = !!INPUT_FILE; @@ -16,408 +16,434 @@ const POSTPROCESS_ONLY = !!INPUT_FILE; * This script creates a JSON file similar to the Java backend's 3rdPartyLicenses.json */ -const OUTPUT_FILE = path.join(__dirname, '..', 'src', 'assets', '3rdPartyLicenses.json'); -const PACKAGE_JSON = path.join(__dirname, '..', 'package.json'); +const OUTPUT_FILE = path.join(__dirname, "..", "src", "assets", "3rdPartyLicenses.json"); +const PACKAGE_JSON = path.join(__dirname, "..", "package.json"); // Ensure the output directory exists const outputDir = path.dirname(OUTPUT_FILE); if (!existsSync(outputDir)) { - mkdirSync(outputDir, { recursive: true }); + mkdirSync(outputDir, { recursive: true }); } -console.log('🔍 Generating frontend license report...'); +console.log("🔍 Generating frontend license report..."); try { - // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK) - if (process.env.PR_IS_FORK === 'true' && !POSTPROCESS_ONLY) { - console.error('Fork PR detected: only --input (postprocess-only) mode is allowed.'); - process.exit(2); + // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK) + if (process.env.PR_IS_FORK === "true" && !POSTPROCESS_ONLY) { + console.error("Fork PR detected: only --input (postprocess-only) mode is allowed."); + process.exit(2); + } + + let licenseData; + // Generate license report using pinned license-checker; disable lifecycle scripts + if (POSTPROCESS_ONLY) { + if (!INPUT_FILE || !existsSync(INPUT_FILE)) { + console.error("❌ --input file missing or not found"); + process.exit(1); } - - let licenseData; - // Generate license report using pinned license-checker; disable lifecycle scripts - if (POSTPROCESS_ONLY) { - if (!INPUT_FILE || !existsSync(INPUT_FILE)) { - console.error('❌ --input file missing or not found'); - process.exit(1); - } - licenseData = JSON.parse(readFileSync(INPUT_FILE, 'utf8')); - } else { - const licenseReport = execSync( - // 'npx --yes license-checker@25.0.1 --production --json', - 'npx --yes license-report --only=prod --output=json', - { - encoding: 'utf8', - cwd: path.dirname(PACKAGE_JSON), - env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: 'true' } - } - ); - try { - licenseData = JSON.parse(licenseReport); - } catch (parseError) { - console.error('❌ Failed to parse license data:', parseError.message); - console.error('Raw output:', licenseReport.substring(0, 500) + '...'); - process.exit(1); - } - } - - if (!Array.isArray(licenseData)) { - console.error('❌ Invalid license data structure'); - process.exit(1); - } - - // Convert license-checker format to array - const licenseArray = licenseData.map(dep => { - let licenseType = dep.licenseType; - - // Handle missing or null licenses - if (!licenseType || licenseType === null || licenseType === undefined) { - licenseType = 'Unknown'; - } - - // Handle empty string licenses - if (licenseType === '') { - licenseType = 'Unknown'; - } - - // Handle array licenses (rare but possible) - if (Array.isArray(licenseType)) { - licenseType = licenseType.join(' AND '); - } - - // Handle object licenses (fallback) - if (typeof licenseType === 'object' && licenseType !== null) { - licenseType = 'Unknown'; - } - - if ( "posthog-js" === dep.name && licenseType.startsWith("SEE LICENSE IN LICENSE")) { - licenseType = "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE"; - } - - return { - name: dep.name, - version: dep.installedVersion || dep.definedVersion || dep.remoteVersion || 'unknown', - licenseType: licenseType, - repository: dep.link, - url: dep.link, - link: dep.link - }; - }); - - // Transform to match Java backend format - const transformedData = { - dependencies: licenseArray.map(dep => { - const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown'); - const licenseUrl = dep.link || getLicenseUrl(licenseType); - - return { - moduleName: dep.name, - moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, - moduleVersion: dep.version, - moduleLicense: licenseType, - moduleLicenseUrl: licenseUrl - }; - }) - }; - - // Log summary of license types found - const licenseSummary = licenseArray.reduce((acc, dep) => { - const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown'); - acc[license] = (acc[license] || 0) + 1; - return acc; - }, {}); - - console.log('📊 License types found:'); - Object.entries(licenseSummary).forEach(([license, count]) => { - console.log(` ${license}: ${count} packages`); - }); - - // Log any complex or unusual license formats for debugging - const complexLicenses = licenseArray.filter(dep => - dep.licenseType && ( - dep.licenseType.includes('AND') || - dep.licenseType.includes('OR') || - dep.licenseType === 'Unknown' || - dep.licenseType.includes('SEE LICENSE') - ) + licenseData = JSON.parse(readFileSync(INPUT_FILE, "utf8")); + } else { + const licenseReport = execSync( + // 'npx --yes license-checker@25.0.1 --production --json', + "npx --yes license-report --only=prod --output=json", + { + encoding: "utf8", + cwd: path.dirname(PACKAGE_JSON), + env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: "true" }, + }, ); - - if (complexLicenses.length > 0) { - console.log('\n🔍 Complex/Edge case licenses detected:'); - complexLicenses.forEach(dep => { - console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`); - }); + try { + licenseData = JSON.parse(licenseReport); + } catch (parseError) { + console.error("❌ Failed to parse license data:", parseError.message); + console.error("Raw output:", licenseReport.substring(0, 500) + "..."); + process.exit(1); } + } - // Check for potentially problematic licenses - const problematicLicenses = checkLicenseCompatibility(licenseSummary, licenseArray); - if (problematicLicenses.length > 0) { - console.log('\n⚠️ License compatibility warnings:'); - problematicLicenses.forEach(warning => { - console.log(` ${warning.message}`); - }); - - // Write license warnings to a separate file for CI/CD - const warningsFile = path.join(__dirname, '..', 'src', 'assets', 'license-warnings.json'); - writeFileSync(warningsFile, JSON.stringify({ - warnings: problematicLicenses, - generated: new Date().toISOString() - }, null, 2)); - console.log(`⚠️ License warnings saved to: ${warningsFile}`); - } else { - console.log('\n✅ All licenses appear to be corporate-friendly'); - } - - // Write to file - writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4)); - - console.log(`✅ License report generated successfully!`); - console.log(`📄 Found ${transformedData.dependencies.length} dependencies`); - console.log(`💾 Saved to: ${OUTPUT_FILE}`); - -} catch (error) { - console.error('❌ Error generating license report:', error.message); + if (!Array.isArray(licenseData)) { + console.error("❌ Invalid license data structure"); process.exit(1); + } + + // Convert license-checker format to array + const licenseArray = licenseData.map((dep) => { + let licenseType = dep.licenseType; + + // Handle missing or null licenses + if (!licenseType || licenseType === null || licenseType === undefined) { + licenseType = "Unknown"; + } + + // Handle empty string licenses + if (licenseType === "") { + licenseType = "Unknown"; + } + + // Handle array licenses (rare but possible) + if (Array.isArray(licenseType)) { + licenseType = licenseType.join(" AND "); + } + + // Handle object licenses (fallback) + if (typeof licenseType === "object" && licenseType !== null) { + licenseType = "Unknown"; + } + + if ("posthog-js" === dep.name && licenseType.startsWith("SEE LICENSE IN LICENSE")) { + licenseType = "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE"; + } + + return { + name: dep.name, + version: dep.installedVersion || dep.definedVersion || dep.remoteVersion || "unknown", + licenseType: licenseType, + repository: dep.link, + url: dep.link, + link: dep.link, + }; + }); + + // Transform to match Java backend format + const transformedData = { + dependencies: licenseArray.map((dep) => { + const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType || "Unknown"; + const licenseUrl = dep.link || getLicenseUrl(licenseType); + + return { + moduleName: dep.name, + moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, + moduleVersion: dep.version, + moduleLicense: licenseType, + moduleLicenseUrl: licenseUrl, + }; + }), + }; + + // Log summary of license types found + const licenseSummary = licenseArray.reduce((acc, dep) => { + const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType || "Unknown"; + acc[license] = (acc[license] || 0) + 1; + return acc; + }, {}); + + console.log("📊 License types found:"); + Object.entries(licenseSummary).forEach(([license, count]) => { + console.log(` ${license}: ${count} packages`); + }); + + // Log any complex or unusual license formats for debugging + const complexLicenses = licenseArray.filter( + (dep) => + dep.licenseType && + (dep.licenseType.includes("AND") || + dep.licenseType.includes("OR") || + dep.licenseType === "Unknown" || + dep.licenseType.includes("SEE LICENSE")), + ); + + if (complexLicenses.length > 0) { + console.log("\n🔍 Complex/Edge case licenses detected:"); + complexLicenses.forEach((dep) => { + console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`); + }); + } + + // Check for potentially problematic licenses + const problematicLicenses = checkLicenseCompatibility(licenseSummary, licenseArray); + if (problematicLicenses.length > 0) { + console.log("\n⚠️ License compatibility warnings:"); + problematicLicenses.forEach((warning) => { + console.log(` ${warning.message}`); + }); + + // Write license warnings to a separate file for CI/CD + const warningsFile = path.join(__dirname, "..", "src", "assets", "license-warnings.json"); + writeFileSync( + warningsFile, + JSON.stringify( + { + warnings: problematicLicenses, + generated: new Date().toISOString(), + }, + null, + 2, + ), + ); + console.log(`⚠️ License warnings saved to: ${warningsFile}`); + } else { + console.log("\n✅ All licenses appear to be corporate-friendly"); + } + + // Write to file + writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 2) + "\n"); + + console.log(`✅ License report generated successfully!`); + console.log(`📄 Found ${transformedData.dependencies.length} dependencies`); + console.log(`💾 Saved to: ${OUTPUT_FILE}`); +} catch (error) { + console.error("❌ Error generating license report:", error.message); + process.exit(1); } /** * Get standard license URLs for common licenses */ function getLicenseUrl(licenseType) { - if (!licenseType || licenseType === 'Unknown') return ''; + if (!licenseType || licenseType === "Unknown") return ""; - const licenseUrls = { - 'MIT': 'https://opensource.org/licenses/MIT', - 'MIT*': 'https://opensource.org/licenses/MIT', - 'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0', - 'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0', - 'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause', - 'BSD-2-Clause': 'https://opensource.org/licenses/BSD-2-Clause', - 'BSD': 'https://opensource.org/licenses/BSD-3-Clause', - 'GPL-3.0': 'https://www.gnu.org/licenses/gpl-3.0.html', - 'GPL-2.0': 'https://www.gnu.org/licenses/gpl-2.0.html', - 'LGPL-2.1': 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html', - 'LGPL-3.0': 'https://www.gnu.org/licenses/lgpl-3.0.html', - 'ISC': 'https://opensource.org/licenses/ISC', - 'CC0-1.0': 'https://creativecommons.org/publicdomain/zero/1.0/', - 'Unlicense': 'https://unlicense.org/', - 'MPL-2.0': 'https://www.mozilla.org/en-US/MPL/2.0/', - 'WTFPL': 'http://www.wtfpl.net/', - 'Zlib': 'https://opensource.org/licenses/Zlib', - 'Artistic-2.0': 'https://opensource.org/licenses/Artistic-2.0', - 'EPL-1.0': 'https://www.eclipse.org/legal/epl-v10.html', - 'EPL-2.0': 'https://www.eclipse.org/legal/epl-2.0/', - 'CDDL-1.0': 'https://opensource.org/licenses/CDDL-1.0', - 'Ruby': 'https://www.ruby-lang.org/en/about/license.txt', - 'Python-2.0': 'https://www.python.org/download/releases/2.0/license/', - 'Public Domain': 'https://creativecommons.org/publicdomain/zero/1.0/', - 'UNLICENSED': '' - }; + const licenseUrls = { + MIT: "https://opensource.org/licenses/MIT", + "MIT*": "https://opensource.org/licenses/MIT", + "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0", + "Apache License 2.0": "https://www.apache.org/licenses/LICENSE-2.0", + "BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause", + "BSD-2-Clause": "https://opensource.org/licenses/BSD-2-Clause", + BSD: "https://opensource.org/licenses/BSD-3-Clause", + "GPL-3.0": "https://www.gnu.org/licenses/gpl-3.0.html", + "GPL-2.0": "https://www.gnu.org/licenses/gpl-2.0.html", + "LGPL-2.1": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html", + "LGPL-3.0": "https://www.gnu.org/licenses/lgpl-3.0.html", + ISC: "https://opensource.org/licenses/ISC", + "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/", + Unlicense: "https://unlicense.org/", + "MPL-2.0": "https://www.mozilla.org/en-US/MPL/2.0/", + WTFPL: "http://www.wtfpl.net/", + Zlib: "https://opensource.org/licenses/Zlib", + "Artistic-2.0": "https://opensource.org/licenses/Artistic-2.0", + "EPL-1.0": "https://www.eclipse.org/legal/epl-v10.html", + "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/", + "CDDL-1.0": "https://opensource.org/licenses/CDDL-1.0", + Ruby: "https://www.ruby-lang.org/en/about/license.txt", + "Python-2.0": "https://www.python.org/download/releases/2.0/license/", + "Public Domain": "https://creativecommons.org/publicdomain/zero/1.0/", + UNLICENSED: "", + }; - // Try exact match first - if (licenseUrls[licenseType]) { - return licenseUrls[licenseType]; + // Try exact match first + if (licenseUrls[licenseType]) { + return licenseUrls[licenseType]; + } + + // Try case-insensitive match + const lowerType = licenseType.toLowerCase(); + for (const [key, url] of Object.entries(licenseUrls)) { + if (key.toLowerCase() === lowerType) { + return url; } + } - // Try case-insensitive match - const lowerType = licenseType.toLowerCase(); - for (const [key, url] of Object.entries(licenseUrls)) { - if (key.toLowerCase() === lowerType) { - return url; - } + // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" + if (licenseType.includes("AND") || licenseType.includes("OR")) { + // Extract the first license from compound expressions for URL + const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); + if (match && licenseUrls[match[1]]) { + return licenseUrls[match[1]]; } + } - // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" - if (licenseType.includes('AND') || licenseType.includes('OR')) { - // Extract the first license from compound expressions for URL - const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); - if (match && licenseUrls[match[1]]) { - return licenseUrls[match[1]]; - } - } - - // For non-standard licenses, return empty string (will use package link if available) - return ''; + // For non-standard licenses, return empty string (will use package link if available) + return ""; } /** * Check for potentially problematic licenses that may not be MIT/corporate compatible */ function checkLicenseCompatibility(licenseSummary, licenseArray) { - const warnings = []; + const warnings = []; - // Define problematic license patterns - const problematicLicenses = { - // Copyleft licenses - 'GPL-2.0': 'Strong copyleft license - requires derivative works to be GPL', - 'GPL-3.0': 'Strong copyleft license - requires derivative works to be GPL', - 'LGPL-2.1': 'Weak copyleft license - may require source disclosure for modifications', - 'LGPL-3.0': 'Weak copyleft license - may require source disclosure for modifications', - 'AGPL-3.0': 'Network copyleft license - requires source disclosure for network use', - 'AGPL-1.0': 'Network copyleft license - requires source disclosure for network use', + // Define problematic license patterns + const problematicLicenses = { + // Copyleft licenses + "GPL-2.0": "Strong copyleft license - requires derivative works to be GPL", + "GPL-3.0": "Strong copyleft license - requires derivative works to be GPL", + "LGPL-2.1": "Weak copyleft license - may require source disclosure for modifications", + "LGPL-3.0": "Weak copyleft license - may require source disclosure for modifications", + "AGPL-3.0": "Network copyleft license - requires source disclosure for network use", + "AGPL-1.0": "Network copyleft license - requires source disclosure for network use", - // Other potentially problematic licenses - 'WTFPL': 'Potentially problematic license - legal uncertainty', - 'CC-BY-SA-4.0': 'ShareAlike license - requires derivative works to use same license', - 'CC-BY-SA-3.0': 'ShareAlike license - requires derivative works to use same license', - 'CC-BY-NC-4.0': 'Non-commercial license - prohibits commercial use', - 'CC-BY-NC-3.0': 'Non-commercial license - prohibits commercial use', - 'OSL-3.0': 'Copyleft license - requires derivative works to be OSL', - 'EPL-1.0': 'Weak copyleft license - may require source disclosure', - 'EPL-2.0': 'Weak copyleft license - may require source disclosure', - 'CDDL-1.0': 'Weak copyleft license - may require source disclosure', - 'CDDL-1.1': 'Weak copyleft license - may require source disclosure', - 'CPL-1.0': 'Weak copyleft license - may require source disclosure', - 'MPL-1.1': 'Weak copyleft license - may require source disclosure', - 'EUPL-1.1': 'Copyleft license - requires derivative works to be EUPL', - 'EUPL-1.2': 'Copyleft license - requires derivative works to be EUPL', - 'UNLICENSED': 'No license specified - usage rights unclear', - 'Unknown': 'License not detected - manual review required' - }; + // Other potentially problematic licenses + WTFPL: "Potentially problematic license - legal uncertainty", + "CC-BY-SA-4.0": "ShareAlike license - requires derivative works to use same license", + "CC-BY-SA-3.0": "ShareAlike license - requires derivative works to use same license", + "CC-BY-NC-4.0": "Non-commercial license - prohibits commercial use", + "CC-BY-NC-3.0": "Non-commercial license - prohibits commercial use", + "OSL-3.0": "Copyleft license - requires derivative works to be OSL", + "EPL-1.0": "Weak copyleft license - may require source disclosure", + "EPL-2.0": "Weak copyleft license - may require source disclosure", + "CDDL-1.0": "Weak copyleft license - may require source disclosure", + "CDDL-1.1": "Weak copyleft license - may require source disclosure", + "CPL-1.0": "Weak copyleft license - may require source disclosure", + "MPL-1.1": "Weak copyleft license - may require source disclosure", + "EUPL-1.1": "Copyleft license - requires derivative works to be EUPL", + "EUPL-1.2": "Copyleft license - requires derivative works to be EUPL", + UNLICENSED: "No license specified - usage rights unclear", + Unknown: "License not detected - manual review required", + }; - // Known good licenses (no warnings needed) - const goodLicenses = new Set([ - 'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD', - 'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0', - 'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0', - 'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE', - 'SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE' - ]); + // Known good licenses (no warnings needed) + const goodLicenses = new Set([ + "MIT", + "MIT*", + "Apache-2.0", + "Apache License 2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "BSD", + "ISC", + "CC0-1.0", + "Public Domain", + "Unlicense", + "0BSD", + "BlueOak-1.0.0", + "Zlib", + "Artistic-2.0", + "Python-2.0", + "Ruby", + "MPL-2.0", + "CC-BY-4.0", + "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", + "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE", + ]); - // Helper function to normalize license names for comparison - function normalizeLicense(license) { - return license - .replace(/-or-later$/, '') // Remove -or-later suffix - .replace(/\+$/, '') // Remove + suffix - .trim(); + // Helper function to normalize license names for comparison + function normalizeLicense(license) { + return license + .replace(/-or-later$/, "") // Remove -or-later suffix + .replace(/\+$/, "") // Remove + suffix + .trim(); + } + + // Check each license type + Object.entries(licenseSummary).forEach(([license, count]) => { + // Skip known good licenses + if (goodLicenses.has(license)) { + return; } - // Check each license type - Object.entries(licenseSummary).forEach(([license, count]) => { - // Skip known good licenses - if (goodLicenses.has(license)) { - return; - } - - // Check if this license only affects our own packages - const affectedPackages = licenseArray.filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }); - - const isOnlyOurPackages = affectedPackages.every(dep => - dep.name === 'frontend' || - dep.name.toLowerCase().includes('stirling-pdf') || - dep.name.toLowerCase().includes('stirling_pdf') || - dep.name.toLowerCase().includes('stirlingpdf') - ); - - if (isOnlyOurPackages && (license === 'UNLICENSED' || license.startsWith('SEE LICENSE IN'))) { - return; // Skip warnings for our own Stirling-PDF packages - } - - // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" - if (license.includes('AND') || license.includes('OR')) { - // For OR licenses, check if there's at least one acceptable license option - if (license.includes('OR')) { - // Extract license components from OR expression - const orComponents = license - .replace(/[()]/g, '') // Remove parentheses - .split(' OR ') - .map(component => component.trim()); - - // Check if any component is in the goodLicenses set (with normalization) - const hasGoodLicense = orComponents.some(component => { - const normalized = normalizeLicense(component); - return goodLicenses.has(component) || goodLicenses.has(normalized); - }); - - if (hasGoodLicense) { - return; // Skip warning - can use the good license option - } - } - - // For AND licenses or OR licenses with no good options, check for problematic components - const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic => - license.includes(problematic) - ); - - if (hasProblematicComponent) { - const affectedPackages = licenseArray - .filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }) - .map(dep => ({ - name: dep.name, - version: dep.version, - url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` - })); - - const licenseType = license.includes('AND') ? 'AND' : 'OR'; - const reason = licenseType === 'AND' - ? 'Compound license with AND requirement - all components must be compatible' - : 'Compound license with potentially problematic components and no good fallback options'; - - warnings.push({ - message: `📋 This PR contains ${count} package${count > 1 ? 's' : ''} with compound license "${license}" - manual review recommended`, - licenseType: license, - licenseUrl: '', - reason: reason, - packageCount: count, - affectedDependencies: affectedPackages - }); - } - return; - } - - // Check for exact matches with problematic licenses - if (problematicLicenses[license]) { - const affectedPackages = licenseArray - .filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }) - .map(dep => ({ - name: dep.name, - version: dep.version, - url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` - })); - - const packageList = affectedPackages.map(pkg => pkg.name).slice(0, 5).join(', ') + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : ''); - const licenseUrl = getLicenseUrl(license) || 'https://opensource.org/licenses'; - - warnings.push({ - message: `⚠️ This PR contains ${count} package${count > 1 ? 's' : ''} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`, - licenseType: license, - licenseUrl: licenseUrl, - reason: problematicLicenses[license], - packageCount: count, - affectedDependencies: affectedPackages - }); - } else { - // Unknown license type - flag for manual review - const affectedPackages = licenseArray - .filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }) - .map(dep => ({ - name: dep.name, - version: dep.version, - url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` - })); - - warnings.push({ - message: `❓ This PR contains ${count} package${count > 1 ? 's' : ''} with unknown license type "${license}" - manual review required`, - licenseType: license, - licenseUrl: '', - reason: 'Unknown license type', - packageCount: count, - affectedDependencies: affectedPackages - }); - } + // Check if this license only affects our own packages + const affectedPackages = licenseArray.filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; + return depLicense === license; }); - return warnings; + const isOnlyOurPackages = affectedPackages.every( + (dep) => + dep.name === "frontend" || + dep.name.toLowerCase().includes("stirling-pdf") || + dep.name.toLowerCase().includes("stirling_pdf") || + dep.name.toLowerCase().includes("stirlingpdf"), + ); + + if (isOnlyOurPackages && (license === "UNLICENSED" || license.startsWith("SEE LICENSE IN"))) { + return; // Skip warnings for our own Stirling-PDF packages + } + + // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" + if (license.includes("AND") || license.includes("OR")) { + // For OR licenses, check if there's at least one acceptable license option + if (license.includes("OR")) { + // Extract license components from OR expression + const orComponents = license + .replace(/[()]/g, "") // Remove parentheses + .split(" OR ") + .map((component) => component.trim()); + + // Check if any component is in the goodLicenses set (with normalization) + const hasGoodLicense = orComponents.some((component) => { + const normalized = normalizeLicense(component); + return goodLicenses.has(component) || goodLicenses.has(normalized); + }); + + if (hasGoodLicense) { + return; // Skip warning - can use the good license option + } + } + + // For AND licenses or OR licenses with no good options, check for problematic components + const hasProblematicComponent = Object.keys(problematicLicenses).some((problematic) => license.includes(problematic)); + + if (hasProblematicComponent) { + const affectedPackages = licenseArray + .filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; + return depLicense === license; + }) + .map((dep) => ({ + name: dep.name, + version: dep.version, + url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, + })); + + const licenseType = license.includes("AND") ? "AND" : "OR"; + const reason = + licenseType === "AND" + ? "Compound license with AND requirement - all components must be compatible" + : "Compound license with potentially problematic components and no good fallback options"; + + warnings.push({ + message: `📋 This PR contains ${count} package${count > 1 ? "s" : ""} with compound license "${license}" - manual review recommended`, + licenseType: license, + licenseUrl: "", + reason: reason, + packageCount: count, + affectedDependencies: affectedPackages, + }); + } + return; + } + + // Check for exact matches with problematic licenses + if (problematicLicenses[license]) { + const affectedPackages = licenseArray + .filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; + return depLicense === license; + }) + .map((dep) => ({ + name: dep.name, + version: dep.version, + url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, + })); + + const packageList = + affectedPackages + .map((pkg) => pkg.name) + .slice(0, 5) + .join(", ") + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : ""); + const licenseUrl = getLicenseUrl(license) || "https://opensource.org/licenses"; + + warnings.push({ + message: `⚠️ This PR contains ${count} package${count > 1 ? "s" : ""} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`, + licenseType: license, + licenseUrl: licenseUrl, + reason: problematicLicenses[license], + packageCount: count, + affectedDependencies: affectedPackages, + }); + } else { + // Unknown license type - flag for manual review + const affectedPackages = licenseArray + .filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; + return depLicense === license; + }) + .map((dep) => ({ + name: dep.name, + version: dep.version, + url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, + })); + + warnings.push({ + message: `❓ This PR contains ${count} package${count > 1 ? "s" : ""} with unknown license type "${license}" - manual review required`, + licenseType: license, + licenseUrl: "", + reason: "Unknown license type", + packageCount: count, + affectedDependencies: affectedPackages, + }); + } + }); + + return warnings; } diff --git a/frontend/scripts/sample-pdf/generate.mjs b/frontend/scripts/sample-pdf/generate.mjs index 93e5cf7ee3..2ad477cc98 100755 --- a/frontend/scripts/sample-pdf/generate.mjs +++ b/frontend/scripts/sample-pdf/generate.mjs @@ -8,20 +8,20 @@ * for users to experiment with Stirling PDF's features. */ -import puppeteer from 'puppeteer'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { existsSync, mkdirSync, statSync } from 'fs'; +import puppeteer from "puppeteer"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { existsSync, mkdirSync, statSync } from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const TEMPLATE_PATH = join(__dirname, 'template.html'); -const OUTPUT_DIR = join(__dirname, '../../public/samples'); -const OUTPUT_PATH = join(OUTPUT_DIR, 'Sample.pdf'); +const TEMPLATE_PATH = join(__dirname, "template.html"); +const OUTPUT_DIR = join(__dirname, "../../public/samples"); +const OUTPUT_PATH = join(OUTPUT_DIR, "Sample.pdf"); async function generatePDF() { - console.log('🚀 Starting Stirling PDF sample document generation...\n'); + console.log("🚀 Starting Stirling PDF sample document generation...\n"); // Ensure output directory exists if (!existsSync(OUTPUT_DIR)) { @@ -40,66 +40,65 @@ async function generatePDF() { let browser; try { // Launch Puppeteer - console.log('🌐 Launching browser...'); + console.log("🌐 Launching browser..."); browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox', '--disable-setuid-sandbox'] + headless: "new", + args: ["--no-sandbox", "--disable-setuid-sandbox"], }); const page = await browser.newPage(); // Set viewport to match A4 proportions await page.setViewport({ - width: 794, // A4 width in pixels at 96 DPI + width: 794, // A4 width in pixels at 96 DPI height: 1123, // A4 height in pixels at 96 DPI - deviceScaleFactor: 2 // Higher quality rendering + deviceScaleFactor: 2, // Higher quality rendering }); // Navigate to the template file const fileUrl = `file://${TEMPLATE_PATH}`; - console.log('📖 Loading HTML template...'); + console.log("📖 Loading HTML template..."); await page.goto(fileUrl, { - waitUntil: 'networkidle0' // Wait for all resources to load + waitUntil: "networkidle0", // Wait for all resources to load }); // Generate PDF with A4 dimensions - console.log('📝 Generating PDF...'); + console.log("📝 Generating PDF..."); await page.pdf({ path: OUTPUT_PATH, - format: 'A4', + format: "A4", printBackground: true, margin: { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, - preferCSSPageSize: true + preferCSSPageSize: true, }); - console.log('\n✅ PDF generated successfully!'); + console.log("\n✅ PDF generated successfully!"); console.log(`📦 Output: ${OUTPUT_PATH}`); // Get file size const stats = statSync(OUTPUT_PATH); const fileSizeInKB = (stats.size / 1024).toFixed(2); console.log(`📊 File size: ${fileSizeInKB} KB`); - } catch (error) { - console.error('\n❌ Error generating PDF:', error.message); + console.error("\n❌ Error generating PDF:", error.message); process.exit(1); } finally { if (browser) { await browser.close(); - console.log('🔒 Browser closed.'); + console.log("🔒 Browser closed."); } } - console.log('\n🎉 Done! Sample PDF is ready for use in Stirling PDF.\n'); + console.log("\n🎉 Done! Sample PDF is ready for use in Stirling PDF.\n"); } // Run the generator -generatePDF().catch(error => { - console.error('Fatal error:', error); +generatePDF().catch((error) => { + console.error("Fatal error:", error); process.exit(1); }); diff --git a/frontend/scripts/sample-pdf/styles.css b/frontend/scripts/sample-pdf/styles.css index 067452833c..7f34b95e8f 100644 --- a/frontend/scripts/sample-pdf/styles.css +++ b/frontend/scripts/sample-pdf/styles.css @@ -20,8 +20,9 @@ --color-white: #ffffff; /* Font Stack */ - --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + --font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; } * { diff --git a/frontend/scripts/sample-pdf/template.html b/frontend/scripts/sample-pdf/template.html index edd7f2c9f4..aea5e4cb13 100644 --- a/frontend/scripts/sample-pdf/template.html +++ b/frontend/scripts/sample-pdf/template.html @@ -1,234 +1,244 @@ - + - - - - Stirling PDF - Sample Document - - - - -
-
- - - - - -
-
-
- + + + + Stirling PDF - Sample Document + + + + +
+
+ + + + +
-

The Free Adobe Acrobat Alternative

-
-
- 10M+ - Downloads +
+
+
-
-
-
Open Source
-
Privacy First
-
Self-Hosted
-
-
-
- - -
-
-

What is Stirling PDF?

-

- Stirling PDF is a robust, web-based PDF manipulation tool. - It enables you to carry out various operations on PDF files, including splitting, - merging, converting, rearranging, adding images, rotating, compressing, and more. -

- -
-
-
- - - +

The Free Adobe Acrobat Alternative

+
+
+ 10M+ + Downloads
-

50+ PDF Operations

-

Comprehensive toolkit covering all your PDF needs. From basic operations to advanced processing.

- -
-
- - - -
-

Workflow Automation

-

Chain multiple operations together and save them as reusable workflows. Perfect for recurring tasks.

-
- -
-
- - - - - -
-

Multi-Language Support

-

Available in over 30 languages with community-contributed translations. Accessible to users worldwide.

-
- -
-
- - - - - -
-

Privacy First

-

Self-hosted solution means your data stays on your infrastructure. You have full control over your documents.

-
- -
-
- - - - -
-

Open Source

-

Transparent, community-driven development. Inspect the code, contribute features, and adapt as needed.

-
- -
-
- - - - -
-

API Access

-

RESTful API for integration with external tools and scripts. Automate PDF operations programmatically.

+
+
Open Source
+
Privacy First
+
Self-Hosted
-
- -
-
-

Key Features

+ +
+
+

What is Stirling PDF?

+

+ Stirling PDF is a robust, web-based PDF manipulation tool. It enables you to carry out various operations on PDF + files, including splitting, merging, converting, rearranging, adding images, rotating, compressing, and more. +

-
-
-
-
+
+
+
- - - - - +
-

Page Operations

+

50+ PDF Operations

+

Comprehensive toolkit covering all your PDF needs. From basic operations to advanced processing.

-
    -
  • Merge & split PDFs
  • -
  • Rearrange pages
  • -
  • Rotate & crop
  • -
  • Extract pages
  • -
  • Multi-page layout
  • -
-
-
-
-
+
+
+ + + +
+

Workflow Automation

+

Chain multiple operations together and save them as reusable workflows. Perfect for recurring tasks.

+
+ +
+
- - + + +
-

Security & Signing

+

Multi-Language Support

+

Available in over 30 languages with community-contributed translations. Accessible to users worldwide.

-
    -
  • Password protection
  • -
  • Digital signatures
  • -
  • Watermarks
  • -
  • Permission controls
  • -
  • Redaction tools
  • -
-
-
-
-
- - +
+
+ + + +
-

File Conversions

+

Privacy First

+

+ Self-hosted solution means your data stays on your infrastructure. You have full control over your documents. +

-
    -
  • PDF to/from images
  • -
  • Office documents
  • -
  • HTML to PDF
  • -
  • Markdown to PDF
  • -
  • PDF to Word/Excel
  • -
-
-
-
-
- - +
+
+ + +
-

Automation

+

Open Source

+

Transparent, community-driven development. Inspect the code, contribute features, and adapt as needed.

-
    -
  • Multi-step workflows
  • -
  • Chain PDF operations
  • -
  • Save recurring tasks
  • -
  • Batch file processing
  • -
  • API integration
  • -
-
-
-
-
-
- - - +
+
+ + + + +
+

API Access

+

RESTful API for integration with external tools and scripts. Automate PDF operations programmatically.

-

Plus Many More

-
-
-
    -
  • OCR text recognition
  • -
  • Compress PDFs
  • -
  • Add images & stamps
  • -
  • Detect blank pages
  • -
  • Extract images
  • -
  • Edit metadata
  • -
-
    -
  • Flatten forms
  • -
  • PDF/A conversion
  • -
  • Add page numbers
  • -
  • Remove pages
  • -
  • Repair PDFs
  • -
  • And 40+ more tools
  • -
-
- + +
+
+

Key Features

+ +
+
+
+
+ + + + + + + +
+

Page Operations

+
+
    +
  • Merge & split PDFs
  • +
  • Rearrange pages
  • +
  • Rotate & crop
  • +
  • Extract pages
  • +
  • Multi-page layout
  • +
+
+ +
+
+
+ + + + +
+

Security & Signing

+
+
    +
  • Password protection
  • +
  • Digital signatures
  • +
  • Watermarks
  • +
  • Permission controls
  • +
  • Redaction tools
  • +
+
+ +
+
+
+ + + +
+

File Conversions

+
+
    +
  • PDF to/from images
  • +
  • Office documents
  • +
  • HTML to PDF
  • +
  • Markdown to PDF
  • +
  • PDF to Word/Excel
  • +
+
+ +
+
+
+ + + +
+

Automation

+
+
    +
  • Multi-step workflows
  • +
  • Chain PDF operations
  • +
  • Save recurring tasks
  • +
  • Batch file processing
  • +
  • API integration
  • +
+
+
+ +
+
+
+ + + +
+

Plus Many More

+
+
+
    +
  • OCR text recognition
  • +
  • Compress PDFs
  • +
  • Add images & stamps
  • +
  • Detect blank pages
  • +
  • Extract images
  • +
  • Edit metadata
  • +
+
    +
  • Flatten forms
  • +
  • PDF/A conversion
  • +
  • Add page numbers
  • +
  • Remove pages
  • +
  • Repair PDFs
  • +
  • And 40+ more tools
  • +
+
+
+
+
+ diff --git a/frontend/scripts/setup-env.ts b/frontend/scripts/setup-env.ts index 00ec03df00..508a3ee19a 100644 --- a/frontend/scripts/setup-env.ts +++ b/frontend/scripts/setup-env.ts @@ -10,22 +10,22 @@ * tsx scripts/setup-env.ts --saas # also checks .env.saas */ -import { existsSync, copyFileSync, readFileSync } from 'fs'; -import { join } from 'path'; -import { config, parse } from 'dotenv'; +import { existsSync, copyFileSync, readFileSync } from "fs"; +import { join } from "path"; +import { config, parse } from "dotenv"; // npm scripts run from the directory containing package.json (frontend/) const root = process.cwd(); const args = process.argv.slice(2); -const isDesktop = args.includes('--desktop'); -const isSaas = args.includes('--saas'); +const isDesktop = args.includes("--desktop"); +const isSaas = args.includes("--saas"); -console.log('setup-env: see frontend/README.md#environment-variables for documentation'); +console.log("setup-env: see frontend/README.md#environment-variables for documentation"); function getExampleKeys(exampleFile: string): string[] { const examplePath = join(root, exampleFile); if (!existsSync(examplePath)) return []; - return Object.keys(parse(readFileSync(examplePath, 'utf-8'))); + return Object.keys(parse(readFileSync(examplePath, "utf-8"))); } function ensureEnvFile(envFile: string, exampleFile: string): boolean { @@ -44,13 +44,13 @@ function ensureEnvFile(envFile: string, exampleFile: string): boolean { config({ path: envPath }); - const missing = getExampleKeys(exampleFile).filter(k => !(k in process.env)); + const missing = getExampleKeys(exampleFile).filter((k) => !(k in process.env)); if (missing.length > 0) { console.error( `setup-env: ${envFile} is missing keys from ${exampleFile}:\n` + - missing.map(k => ` ${k}`).join('\n') + - '\n Add them manually or delete your local file to re-copy from the example.' + missing.map((k) => ` ${k}`).join("\n") + + "\n Add them manually or delete your local file to re-copy from the example.", ); return true; } @@ -59,29 +59,28 @@ function ensureEnvFile(envFile: string, exampleFile: string): boolean { } let failed = false; -failed = ensureEnvFile('.env', 'config/.env.example') || failed; +failed = ensureEnvFile(".env", "config/.env.example") || failed; if (isDesktop) { - failed = ensureEnvFile('.env.desktop', 'config/.env.desktop.example') || failed; + failed = ensureEnvFile(".env.desktop", "config/.env.desktop.example") || failed; } if (isSaas) { - failed = ensureEnvFile('.env.saas', 'config/.env.saas.example') || failed; + failed = ensureEnvFile(".env.saas", "config/.env.saas.example") || failed; } // Warn about any VITE_ vars set in the environment that aren't listed in any example file. const allExampleKeys = new Set([ - ...getExampleKeys('config/.env.example'), - ...getExampleKeys('config/.env.desktop.example'), - ...getExampleKeys('config/.env.saas.example'), + ...getExampleKeys("config/.env.example"), + ...getExampleKeys("config/.env.desktop.example"), + ...getExampleKeys("config/.env.saas.example"), ]); -const unknownViteVars = Object.keys(process.env) - .filter(k => k.startsWith('VITE_') && !allExampleKeys.has(k)); +const unknownViteVars = Object.keys(process.env).filter((k) => k.startsWith("VITE_") && !allExampleKeys.has(k)); if (unknownViteVars.length > 0) { console.warn( - 'setup-env: the following VITE_ vars are set but not listed in any example file:\n' + - unknownViteVars.map(k => ` ${k}`).join('\n') + - '\n Add them to the appropriate config/.env.*.example file if they are required.' + "setup-env: the following VITE_ vars are set but not listed in any example file:\n" + + unknownViteVars.map((k) => ` ${k}`).join("\n") + + "\n Add them to the appropriate config/.env.*.example file if they are required.", ); } diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index 6acaac5145..9259e4543f 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -2,9 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "enables the default permissions", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:default", "core:window:allow-destroy", diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 10203960c0..536d0d382d 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,98 +1,82 @@ { - "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", - "productName": "Stirling-PDF", - "version": "2.9.2", - "identifier": "stirling.pdf.dev", - "build": { - "frontendDist": "../dist", - "devUrl": "http://localhost:5173", - "beforeDevCommand": "npm run dev -- --mode desktop", - "beforeBuildCommand": "node scripts/build-provisioner.mjs && npm run build -- --mode desktop" + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "Stirling-PDF", + "version": "2.9.2", + "identifier": "stirling.pdf.dev", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:5173", + "beforeDevCommand": "npm run dev -- --mode desktop", + "beforeBuildCommand": "node scripts/build-provisioner.mjs && npm run build -- --mode desktop" + }, + "app": { + "windows": [ + { + "title": "Stirling-PDF", + "width": 1280, + "height": 800, + "resizable": true, + "fullscreen": false, + "additionalBrowserArgs": "--enable-features=CertVerifierBuiltinFeature" + } + ] + }, + "bundle": { + "active": true, + "publisher": "Stirling PDF Inc.", + "targets": ["deb", "rpm", "dmg", "msi"], + "icon": [ + "icons/icon.png", + "icons/icon.icns", + "icons/icon.ico", + "icons/16x16.png", + "icons/32x32.png", + "icons/64x64.png", + "icons/128x128.png", + "icons/192x192.png" + ], + "resources": ["libs/*.jar", "runtime/jre/**/*"], + "fileAssociations": [ + { + "ext": ["pdf"], + "name": "PDF Document", + "role": "Editor", + "mimeType": "application/pdf" + } + ], + "linux": { + "deb": { + "desktopTemplate": "stirling-pdf.desktop" + } }, - "app": { - "windows": [ - { - "title": "Stirling-PDF", - "width": 1280, - "height": 800, - "resizable": true, - "fullscreen": false, - "additionalBrowserArgs": "--enable-features=CertVerifierBuiltinFeature" - } - ] + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "http://timestamp.digicert.com", + "wix": { + "fragmentPaths": ["windows/wix/provisioning.wxs"], + "componentGroupRefs": ["ProvisioningComponentGroup"] + } }, - "bundle": { - "active": true, - "publisher": "Stirling PDF Inc.", - "targets": [ - "deb", - "rpm", - "dmg", - "msi" - ], - "icon": [ - "icons/icon.png", - "icons/icon.icns", - "icons/icon.ico", - "icons/16x16.png", - "icons/32x32.png", - "icons/64x64.png", - "icons/128x128.png", - "icons/192x192.png" - ], - "resources": [ - "libs/*.jar", - "runtime/jre/**/*" - ], - "fileAssociations": [ - { - "ext": [ - "pdf" - ], - "name": "PDF Document", - "role": "Editor", - "mimeType": "application/pdf" - } - ], - "linux": { - "deb": { - "desktopTemplate": "stirling-pdf.desktop" - } - }, - "windows": { - "certificateThumbprint": null, - "digestAlgorithm": "sha256", - "timestampUrl": "http://timestamp.digicert.com", - "wix": { - "fragmentPaths": [ - "windows/wix/provisioning.wxs" - ], - "componentGroupRefs": [ - "ProvisioningComponentGroup" - ] - } - }, - "macOS": { - "minimumSystemVersion": "10.15", - "signingIdentity": null, - "entitlements": null, - "providerShortName": null, - "infoPlist": "Info.plist" - } - }, - "plugins": { - "shell": { - "open": true - }, - "fs": { - "requireLiteralLeadingDot": false - }, - "deep-link": { - "desktop": { - "schemes": [ - "stirlingpdf" - ] - } - } + "macOS": { + "minimumSystemVersion": "10.15", + "signingIdentity": null, + "entitlements": null, + "providerShortName": null, + "infoPlist": "Info.plist" } + }, + "plugins": { + "shell": { + "open": true + }, + "fs": { + "requireLiteralLeadingDot": false + }, + "deep-link": { + "desktop": { + "schemes": ["stirlingpdf"] + } + } + } } diff --git a/frontend/src/assets/3rdPartyLicenses.json b/frontend/src/assets/3rdPartyLicenses.json index 392249d420..baf60879b6 100644 --- a/frontend/src/assets/3rdPartyLicenses.json +++ b/frontend/src/assets/3rdPartyLicenses.json @@ -1,326 +1,326 @@ { - "dependencies": [ - { - "moduleName": "@atlaskit/pragmatic-drag-and-drop", - "moduleUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git", - "moduleVersion": "1.7.7", - "moduleLicense": "Apache-2.0", - "moduleLicenseUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git" - }, - { - "moduleName": "@embedpdf/core", - "moduleUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz" - }, - { - "moduleName": "@embedpdf/engines", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-annotation", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-export", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-history", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-interaction-manager", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-loader", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-pan", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-render", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-rotate", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-scroll", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-search", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-selection", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-spread", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-thumbnail", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-tiling", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-viewport", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@embedpdf/plugin-zoom", - "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", - "moduleVersion": "1.3.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" - }, - { - "moduleName": "@emotion/react", - "moduleUrl": "git+https://github.com/emotion-js/emotion.git#main", - "moduleVersion": "11.14.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/emotion-js/emotion.git#main" - }, - { - "moduleName": "@emotion/styled", - "moduleUrl": "git+https://github.com/emotion-js/emotion.git#main", - "moduleVersion": "11.14.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/emotion-js/emotion.git#main" - }, - { - "moduleName": "@iconify/react", - "moduleUrl": "git+https://github.com/iconify/iconify.git", - "moduleVersion": "6.0.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/iconify/iconify.git" - }, - { - "moduleName": "@mantine/core", - "moduleUrl": "git+https://github.com/mantinedev/mantine.git", - "moduleVersion": "8.3.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" - }, - { - "moduleName": "@mantine/dates", - "moduleUrl": "git+https://github.com/mantinedev/mantine.git", - "moduleVersion": "8.3.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" - }, - { - "moduleName": "@mantine/dropzone", - "moduleUrl": "git+https://github.com/mantinedev/mantine.git", - "moduleVersion": "8.3.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" - }, - { - "moduleName": "@mantine/hooks", - "moduleUrl": "git+https://github.com/mantinedev/mantine.git", - "moduleVersion": "8.3.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" - }, - { - "moduleName": "@mui/icons-material", - "moduleUrl": "git+https://github.com/mui/material-ui.git", - "moduleVersion": "7.3.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/mui/material-ui.git" - }, - { - "moduleName": "@mui/material", - "moduleUrl": "git+https://github.com/mui/material-ui.git", - "moduleVersion": "7.3.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/mui/material-ui.git" - }, - { - "moduleName": "@tailwindcss/postcss", - "moduleUrl": "git+https://github.com/tailwindlabs/tailwindcss.git", - "moduleVersion": "4.1.13", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/tailwindlabs/tailwindcss.git" - }, - { - "moduleName": "@tanstack/react-virtual", - "moduleUrl": "git+https://github.com/TanStack/virtual.git", - "moduleVersion": "3.13.12", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/TanStack/virtual.git" - }, - { - "moduleName": "autoprefixer", - "moduleUrl": "git+https://github.com/postcss/autoprefixer.git", - "moduleVersion": "10.4.21", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/postcss/autoprefixer.git" - }, - { - "moduleName": "axios", - "moduleUrl": "git+https://github.com/axios/axios.git", - "moduleVersion": "1.12.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/axios/axios.git" - }, - { - "moduleName": "i18next", - "moduleUrl": "git+https://github.com/i18next/i18next.git", - "moduleVersion": "25.5.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/i18next/i18next.git" - }, - { - "moduleName": "i18next-browser-languagedetector", - "moduleUrl": "git+https://github.com/i18next/i18next-browser-languageDetector.git", - "moduleVersion": "8.2.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/i18next/i18next-browser-languageDetector.git" - }, - { - "moduleName": "i18next-http-backend", - "moduleUrl": "git+ssh://git@github.com/i18next/i18next-http-backend.git", - "moduleVersion": "3.0.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+ssh://git@github.com/i18next/i18next-http-backend.git" - }, - { - "moduleName": "jszip", - "moduleUrl": "git+https://github.com/Stuk/jszip.git", - "moduleVersion": "3.10.1", - "moduleLicense": "(MIT OR GPL-3.0-or-later)", - "moduleLicenseUrl": "git+https://github.com/Stuk/jszip.git" - }, - { - "moduleName": "license-report", - "moduleUrl": "git+https://github.com/kessler/license-report.git", - "moduleVersion": "6.8.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/kessler/license-report.git" - }, - { - "moduleName": "pdf-lib", - "moduleUrl": "git+https://github.com/Hopding/pdf-lib.git", - "moduleVersion": "1.17.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/Hopding/pdf-lib.git" - }, - { - "moduleName": "pdfjs-dist", - "moduleUrl": "git+https://github.com/mozilla/pdf.js.git", - "moduleVersion": "5.4.149", - "moduleLicense": "Apache-2.0", - "moduleLicenseUrl": "git+https://github.com/mozilla/pdf.js.git" - }, - { - "moduleName": "posthog-js", - "moduleUrl": "git+https://github.com/PostHog/posthog-js.git", - "moduleVersion": "1.268.0", - "moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE", - "moduleLicenseUrl": "git+https://github.com/PostHog/posthog-js.git" - }, - { - "moduleName": "react", - "moduleUrl": "git+https://github.com/facebook/react.git", - "moduleVersion": "19.1.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/facebook/react.git" - }, - { - "moduleName": "react-dom", - "moduleUrl": "git+https://github.com/facebook/react.git", - "moduleVersion": "19.1.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/facebook/react.git" - }, - { - "moduleName": "react-i18next", - "moduleUrl": "git+https://github.com/i18next/react-i18next.git", - "moduleVersion": "15.7.3", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/i18next/react-i18next.git" - }, - { - "moduleName": "react-router-dom", - "moduleUrl": "git+https://github.com/remix-run/react-router.git", - "moduleVersion": "7.9.1", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/remix-run/react-router.git" - }, - { - "moduleName": "tailwindcss", - "moduleUrl": "git+https://github.com/tailwindlabs/tailwindcss.git", - "moduleVersion": "4.1.13", - "moduleLicense": "MIT", - "moduleLicenseUrl": "git+https://github.com/tailwindlabs/tailwindcss.git" - }, - { - "moduleName": "web-vitals", - "moduleUrl": "git+https://github.com/GoogleChrome/web-vitals.git", - "moduleVersion": "5.1.0", - "moduleLicense": "Apache-2.0", - "moduleLicenseUrl": "git+https://github.com/GoogleChrome/web-vitals.git" - } - ] -} \ No newline at end of file + "dependencies": [ + { + "moduleName": "@atlaskit/pragmatic-drag-and-drop", + "moduleUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git", + "moduleVersion": "1.7.7", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git" + }, + { + "moduleName": "@embedpdf/core", + "moduleUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz" + }, + { + "moduleName": "@embedpdf/engines", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-annotation", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-export", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-history", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-interaction-manager", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-loader", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-pan", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-render", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-rotate", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-scroll", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-search", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-selection", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-spread", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-thumbnail", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-tiling", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-viewport", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@embedpdf/plugin-zoom", + "moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git" + }, + { + "moduleName": "@emotion/react", + "moduleUrl": "git+https://github.com/emotion-js/emotion.git#main", + "moduleVersion": "11.14.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/emotion-js/emotion.git#main" + }, + { + "moduleName": "@emotion/styled", + "moduleUrl": "git+https://github.com/emotion-js/emotion.git#main", + "moduleVersion": "11.14.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/emotion-js/emotion.git#main" + }, + { + "moduleName": "@iconify/react", + "moduleUrl": "git+https://github.com/iconify/iconify.git", + "moduleVersion": "6.0.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/iconify/iconify.git" + }, + { + "moduleName": "@mantine/core", + "moduleUrl": "git+https://github.com/mantinedev/mantine.git", + "moduleVersion": "8.3.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" + }, + { + "moduleName": "@mantine/dates", + "moduleUrl": "git+https://github.com/mantinedev/mantine.git", + "moduleVersion": "8.3.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" + }, + { + "moduleName": "@mantine/dropzone", + "moduleUrl": "git+https://github.com/mantinedev/mantine.git", + "moduleVersion": "8.3.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" + }, + { + "moduleName": "@mantine/hooks", + "moduleUrl": "git+https://github.com/mantinedev/mantine.git", + "moduleVersion": "8.3.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git" + }, + { + "moduleName": "@mui/icons-material", + "moduleUrl": "git+https://github.com/mui/material-ui.git", + "moduleVersion": "7.3.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/mui/material-ui.git" + }, + { + "moduleName": "@mui/material", + "moduleUrl": "git+https://github.com/mui/material-ui.git", + "moduleVersion": "7.3.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/mui/material-ui.git" + }, + { + "moduleName": "@tailwindcss/postcss", + "moduleUrl": "git+https://github.com/tailwindlabs/tailwindcss.git", + "moduleVersion": "4.1.13", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/tailwindlabs/tailwindcss.git" + }, + { + "moduleName": "@tanstack/react-virtual", + "moduleUrl": "git+https://github.com/TanStack/virtual.git", + "moduleVersion": "3.13.12", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/TanStack/virtual.git" + }, + { + "moduleName": "autoprefixer", + "moduleUrl": "git+https://github.com/postcss/autoprefixer.git", + "moduleVersion": "10.4.21", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/postcss/autoprefixer.git" + }, + { + "moduleName": "axios", + "moduleUrl": "git+https://github.com/axios/axios.git", + "moduleVersion": "1.12.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/axios/axios.git" + }, + { + "moduleName": "i18next", + "moduleUrl": "git+https://github.com/i18next/i18next.git", + "moduleVersion": "25.5.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/i18next/i18next.git" + }, + { + "moduleName": "i18next-browser-languagedetector", + "moduleUrl": "git+https://github.com/i18next/i18next-browser-languageDetector.git", + "moduleVersion": "8.2.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/i18next/i18next-browser-languageDetector.git" + }, + { + "moduleName": "i18next-http-backend", + "moduleUrl": "git+ssh://git@github.com/i18next/i18next-http-backend.git", + "moduleVersion": "3.0.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+ssh://git@github.com/i18next/i18next-http-backend.git" + }, + { + "moduleName": "jszip", + "moduleUrl": "git+https://github.com/Stuk/jszip.git", + "moduleVersion": "3.10.1", + "moduleLicense": "(MIT OR GPL-3.0-or-later)", + "moduleLicenseUrl": "git+https://github.com/Stuk/jszip.git" + }, + { + "moduleName": "license-report", + "moduleUrl": "git+https://github.com/kessler/license-report.git", + "moduleVersion": "6.8.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/kessler/license-report.git" + }, + { + "moduleName": "pdf-lib", + "moduleUrl": "git+https://github.com/Hopding/pdf-lib.git", + "moduleVersion": "1.17.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/Hopding/pdf-lib.git" + }, + { + "moduleName": "pdfjs-dist", + "moduleUrl": "git+https://github.com/mozilla/pdf.js.git", + "moduleVersion": "5.4.149", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "git+https://github.com/mozilla/pdf.js.git" + }, + { + "moduleName": "posthog-js", + "moduleUrl": "git+https://github.com/PostHog/posthog-js.git", + "moduleVersion": "1.268.0", + "moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE", + "moduleLicenseUrl": "git+https://github.com/PostHog/posthog-js.git" + }, + { + "moduleName": "react", + "moduleUrl": "git+https://github.com/facebook/react.git", + "moduleVersion": "19.1.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/facebook/react.git" + }, + { + "moduleName": "react-dom", + "moduleUrl": "git+https://github.com/facebook/react.git", + "moduleVersion": "19.1.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/facebook/react.git" + }, + { + "moduleName": "react-i18next", + "moduleUrl": "git+https://github.com/i18next/react-i18next.git", + "moduleVersion": "15.7.3", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/i18next/react-i18next.git" + }, + { + "moduleName": "react-router-dom", + "moduleUrl": "git+https://github.com/remix-run/react-router.git", + "moduleVersion": "7.9.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/remix-run/react-router.git" + }, + { + "moduleName": "tailwindcss", + "moduleUrl": "git+https://github.com/tailwindlabs/tailwindcss.git", + "moduleVersion": "4.1.13", + "moduleLicense": "MIT", + "moduleLicenseUrl": "git+https://github.com/tailwindlabs/tailwindcss.git" + }, + { + "moduleName": "web-vitals", + "moduleUrl": "git+https://github.com/GoogleChrome/web-vitals.git", + "moduleVersion": "5.1.0", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "git+https://github.com/GoogleChrome/web-vitals.git" + } + ] +} diff --git a/frontend/src/core/App.tsx b/frontend/src/core/App.tsx index f53031c4ba..c417ef1f65 100644 --- a/frontend/src/core/App.tsx +++ b/frontend/src/core/App.tsx @@ -21,9 +21,7 @@ import "@app/utils/fileIdSafety"; function MobileScannerProviders({ children }: { children: React.ReactNode }) { return ( - - {children} - + {children} ); } diff --git a/frontend/src/core/components/AppLayout.tsx b/frontend/src/core/components/AppLayout.tsx index 9bcd31e6db..328758cf3a 100644 --- a/frontend/src/core/components/AppLayout.tsx +++ b/frontend/src/core/components/AppLayout.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; -import { useBanner } from '@app/contexts/BannerContext'; -import NavigationWarningModal from '@app/components/shared/NavigationWarningModal'; +import { ReactNode } from "react"; +import { useBanner } from "@app/contexts/BannerContext"; +import NavigationWarningModal from "@app/components/shared/NavigationWarningModal"; interface AppLayoutProps { children: ReactNode; @@ -21,11 +21,9 @@ export function AppLayout({ children }: AppLayoutProps) { height: 100% !important; } `} -
+
{banner} -
- {children} -
+
{children}
diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 75c7d281c3..3080f946c0 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -8,7 +8,12 @@ import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext"; import { HotkeyProvider } from "@app/contexts/HotkeyContext"; import { SidebarProvider } from "@app/contexts/SidebarContext"; import { PreferencesProvider, usePreferences } from "@app/contexts/PreferencesContext"; -import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions, useAppConfig } from "@app/contexts/AppConfigContext"; +import { + AppConfigProvider, + AppConfigProviderProps, + AppConfigRetryOptions, + useAppConfig, +} from "@app/contexts/AppConfigContext"; import { RightRailProvider } from "@app/contexts/RightRailContext"; import { ViewerProvider } from "@app/contexts/ViewerContext"; import { SignatureProvider } from "@app/contexts/SignatureContext"; @@ -20,8 +25,8 @@ import { BannerProvider } from "@app/contexts/BannerContext"; import ErrorBoundary from "@app/components/shared/ErrorBoundary"; import { useScarfTracking } from "@app/hooks/useScarfTracking"; import { useAppInitialization } from "@app/hooks/useAppInitialization"; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import AppConfigLoader from '@app/components/shared/AppConfigLoader'; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; +import AppConfigLoader from "@app/components/shared/AppConfigLoader"; import { RedactionProvider } from "@app/contexts/RedactionContext"; import { FormFillProvider } from "@app/tools/formFill/FormFillContext"; @@ -41,14 +46,14 @@ function BrandingAssetManager() { const { favicon, logo192, manifestHref } = useLogoAssets(); useEffect(() => { - if (typeof document === 'undefined') { + if (typeof document === "undefined") { return; } const setLinkHref = (selector: string, href: string) => { const link = document.querySelector(selector); - if (link && link.getAttribute('href') !== href) { - link.setAttribute('href', href); + if (link && link.getAttribute("href") !== href) { + link.setAttribute("href", href); } }; @@ -62,7 +67,7 @@ function BrandingAssetManager() { } // Avoid requirement to have props which are required in app providers anyway -type AppConfigProviderOverrides = Omit; +type AppConfigProviderOverrides = Omit; export interface AppProvidersProps { children: ReactNode; @@ -98,49 +103,44 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - {children} - + {children} - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/frontend/src/core/components/FileManager.tsx b/frontend/src/core/components/FileManager.tsx index e3c9a84661..fe140ab173 100644 --- a/frontend/src/core/components/FileManager.tsx +++ b/frontend/src/core/components/FileManager.tsx @@ -1,19 +1,19 @@ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; -import { Modal } from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import { StirlingFileStub } from '@app/types/fileContext'; -import { useFileManager } from '@app/hooks/useFileManager'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { Tool } from '@app/types/tool'; -import MobileLayout from '@app/components/fileManager/MobileLayout'; -import DesktopLayout from '@app/components/fileManager/DesktopLayout'; -import DragOverlay from '@app/components/fileManager/DragOverlay'; -import { FileManagerProvider } from '@app/contexts/FileManagerContext'; -import { Z_INDEX_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { isGoogleDriveConfigured, extractGoogleDriveBackendConfig } from '@app/services/googleDrivePickerService'; -import { loadScript } from '@app/utils/scriptLoader'; -import { useAllFiles } from '@app/contexts/FileContext'; +import React, { useState, useCallback, useEffect, useMemo } from "react"; +import { Modal } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import { StirlingFileStub } from "@app/types/fileContext"; +import { useFileManager } from "@app/hooks/useFileManager"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { Tool } from "@app/types/tool"; +import MobileLayout from "@app/components/fileManager/MobileLayout"; +import DesktopLayout from "@app/components/fileManager/DesktopLayout"; +import DragOverlay from "@app/components/fileManager/DragOverlay"; +import { FileManagerProvider } from "@app/contexts/FileManagerContext"; +import { Z_INDEX_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { isGoogleDriveConfigured, extractGoogleDriveBackendConfig } from "@app/services/googleDrivePickerService"; +import { loadScript } from "@app/utils/scriptLoader"; +import { useAllFiles } from "@app/contexts/FileContext"; interface FileManagerProps { selectedTool?: Tool | null; @@ -32,47 +32,59 @@ const FileManager: React.FC = ({ selectedTool }) => { const { fileIds: activeFileIds } = useAllFiles(); // File management handlers - const isFileSupported = useCallback((fileName: string) => { - if (!selectedTool?.supportedFormats) return true; - const extension = fileName.split('.').pop()?.toLowerCase(); - return selectedTool.supportedFormats.includes(extension || ''); - }, [selectedTool?.supportedFormats]); + const isFileSupported = useCallback( + (fileName: string) => { + if (!selectedTool?.supportedFormats) return true; + const extension = fileName.split(".").pop()?.toLowerCase(); + return selectedTool.supportedFormats.includes(extension || ""); + }, + [selectedTool?.supportedFormats], + ); const refreshRecentFiles = useCallback(async () => { const files = await loadRecentFiles(); setRecentFiles(files); }, [loadRecentFiles]); - const handleRecentFilesSelected = useCallback(async (files: StirlingFileStub[]) => { - try { - // Use StirlingFileStubs directly - preserves all metadata! - onRecentFileSelect(files); - } catch (error) { - console.error('Failed to process selected files:', error); - } - }, [onRecentFileSelect]); - - const handleNewFileUpload = useCallback(async (files: File[]) => { - if (files.length > 0) { + const handleRecentFilesSelected = useCallback( + async (files: StirlingFileStub[]) => { try { - // Files will get IDs assigned through onFilesSelect -> FileContext addFiles - onFileUpload(files); - await refreshRecentFiles(); + // Use StirlingFileStubs directly - preserves all metadata! + onRecentFileSelect(files); } catch (error) { - console.error('Failed to process dropped files:', error); + console.error("Failed to process selected files:", error); } - } - }, [onFileUpload, refreshRecentFiles]); + }, + [onRecentFileSelect], + ); - const handleRemoveFileByIndex = useCallback(async (index: number) => { - await handleRemoveFile(index, recentFiles, setRecentFiles); - }, [handleRemoveFile, recentFiles]); + const handleNewFileUpload = useCallback( + async (files: File[]) => { + if (files.length > 0) { + try { + // Files will get IDs assigned through onFilesSelect -> FileContext addFiles + onFileUpload(files); + await refreshRecentFiles(); + } catch (error) { + console.error("Failed to process dropped files:", error); + } + } + }, + [onFileUpload, refreshRecentFiles], + ); + + const handleRemoveFileByIndex = useCallback( + async (index: number) => { + await handleRemoveFile(index, recentFiles, setRecentFiles); + }, + [handleRemoveFile, recentFiles], + ); useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 1030); checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); }, []); useEffect(() => { @@ -89,7 +101,7 @@ const FileManager: React.FC = ({ selectedTool }) => { return () => { // StoredFileMetadata doesn't have blob URLs, so no cleanup needed // Blob URLs are managed by FileContext and tool operations - console.log('FileManager unmounting - FileContext handles blob URL cleanup'); + console.log("FileManager unmounting - FileContext handles blob URL cleanup"); }; }, []); @@ -97,7 +109,7 @@ const FileManager: React.FC = ({ selectedTool }) => { // Use useMemo to only track Google Drive config changes, not all config updates const googleDriveBackendConfig = useMemo( () => extractGoogleDriveBackendConfig(config), - [config?.googleDriveEnabled, config?.googleDriveClientId, config?.googleDriveApiKey, config?.googleDriveAppId] + [config?.googleDriveEnabled, config?.googleDriveClientId, config?.googleDriveApiKey, config?.googleDriveAppId], ); useEffect(() => { @@ -105,29 +117,29 @@ const FileManager: React.FC = ({ selectedTool }) => { // Load scripts in parallel without blocking Promise.all([ loadScript({ - src: 'https://apis.google.com/js/api.js', - id: 'gapi-script', + src: "https://apis.google.com/js/api.js", + id: "gapi-script", async: true, defer: true, }), loadScript({ - src: 'https://accounts.google.com/gsi/client', - id: 'gis-script', + src: "https://accounts.google.com/gsi/client", + id: "gis-script", async: true, defer: true, }), ]).catch((error) => { - console.warn('Failed to preload Google Drive scripts:', error); + console.warn("Failed to preload Google Drive scripts:", error); }); } }, [googleDriveBackendConfig]); // Modal size constants for consistent scaling - const modalHeight = '80vh'; - const modalWidth = isMobile ? '100%' : '80vw'; - const modalMaxWidth = isMobile ? '100%' : '1200px'; - const modalMaxHeight = '1200px'; - const modalMinWidth = isMobile ? '320px' : '800px'; + const modalHeight = "80vh"; + const modalWidth = isMobile ? "100%" : "80vw"; + const modalMaxWidth = isMobile ? "100%" : "1200px"; + const modalMaxHeight = "1200px"; + const modalMinWidth = isMobile ? "320px" : "800px"; return ( = ({ selectedTool }) => { zIndex={Z_INDEX_FILE_MANAGER_MODAL} styles={{ content: { - position: 'relative', - margin: isMobile ? '1rem' : '2rem' + position: "relative", + margin: isMobile ? "1rem" : "2rem", }, body: { padding: 0 }, - header: { display: 'none' } + header: { display: "none" }, }} > -
+
setIsDragging(true)} @@ -165,14 +179,14 @@ const FileManager: React.FC = ({ selectedTool }) => { multiple={true} activateOnClick={false} style={{ - height: '100%', - width: '100%', - border: 'none', - borderRadius: 'var(--radius-md)', - backgroundColor: 'var(--bg-file-manager)' + height: "100%", + width: "100%", + border: "none", + borderRadius: "var(--radius-md)", + backgroundColor: "var(--bg-file-manager)", }} styles={{ - inner: { pointerEvents: 'all' } + inner: { pointerEvents: "all" }, }} > void; } -const StorageStatsCard: React.FC = ({ - storageStats, - filesCount, - onClearAll, - onReloadFiles, -}) => { +const StorageStatsCard: React.FC = ({ storageStats, filesCount, onClearAll, onReloadFiles }) => { const { t } = useTranslation(); if (!storageStats) return null; @@ -59,12 +54,7 @@ const StorageStatsCard: React.FC = ({ {t("fileManager.clearAll", "Clear All")} )} - @@ -73,4 +63,4 @@ const StorageStatsCard: React.FC = ({ ); }; -export default StorageStatsCard; \ No newline at end of file +export default StorageStatsCard; diff --git a/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx b/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx index 0979d59e35..d1c8f5c087 100644 --- a/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx +++ b/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, ReactNode } from 'react'; +import React, { createContext, useContext, ReactNode } from "react"; interface PDFAnnotationContextValue { // Drawing mode management @@ -58,7 +58,7 @@ export const PDFAnnotationProvider: React.FC = ({ getImageData, isPlacementMode, signatureConfig, - setSignatureConfig + setSignatureConfig, }) => { const contextValue: PDFAnnotationContextValue = { activateDrawMode, @@ -72,20 +72,16 @@ export const PDFAnnotationProvider: React.FC = ({ getImageData, isPlacementMode, signatureConfig, - setSignatureConfig + setSignatureConfig, }; - return ( - - {children} - - ); + return {children}; }; export const usePDFAnnotation = (): PDFAnnotationContextValue => { const context = useContext(PDFAnnotationContext); if (context === undefined) { - throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider'); + throw new Error("usePDFAnnotation must be used within a PDFAnnotationProvider"); } return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx index ea093b5be8..07a441abd5 100644 --- a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx +++ b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, Alert, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { DrawingControls } from '@app/components/annotation/shared/DrawingControls'; -import { ColorPicker } from '@app/components/annotation/shared/ColorPicker'; -import { usePDFAnnotation } from '@app/components/annotation/providers/PDFAnnotationProvider'; -import { useSignature } from '@app/contexts/SignatureContext'; +import React, { useEffect, useState } from "react"; +import { Stack, Alert, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { DrawingControls } from "@app/components/annotation/shared/DrawingControls"; +import { ColorPicker } from "@app/components/annotation/shared/ColorPicker"; +import { usePDFAnnotation } from "@app/components/annotation/providers/PDFAnnotationProvider"; +import { useSignature } from "@app/contexts/SignatureContext"; export interface AnnotationToolConfig { enableDrawing?: boolean; @@ -25,17 +25,13 @@ export const BaseAnnotationTool: React.FC = ({ config, children, onSignatureDataChange, - disabled = false + disabled = false, }) => { const { t } = useTranslation(); - const { - activateSignaturePlacementMode, - undo, - redo - } = usePDFAnnotation(); + const { activateSignaturePlacementMode, undo, redo } = usePDFAnnotation(); const { historyApiRef } = useSignature(); - const [selectedColor, setSelectedColor] = useState('#000000'); + const [selectedColor, setSelectedColor] = useState("#000000"); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [signatureData, setSignatureData] = useState(null); const [historyAvailability, setHistoryAvailability] = useState({ canUndo: false, canRedo: false }); @@ -94,14 +90,12 @@ export const BaseAnnotationTool: React.FC = ({ signatureData, onSignatureDataChange: handleSignatureDataChange, onColorSwatchClick: () => setIsColorPickerOpen(true), - disabled + disabled, })} {/* Instructions for placing signature */} - - - Click anywhere on the PDF to place your annotation. - + + Click anywhere on the PDF to place your annotation. {/* Color Picker Modal */} diff --git a/frontend/src/core/components/annotation/shared/ColorControl.tsx b/frontend/src/core/components/annotation/shared/ColorControl.tsx index 16b3f845bb..66ff868fd0 100644 --- a/frontend/src/core/components/annotation/shared/ColorControl.tsx +++ b/frontend/src/core/components/annotation/shared/ColorControl.tsx @@ -1,15 +1,15 @@ -import { ActionIcon, Tooltip, Popover, Stack, ColorSwatch, ColorPicker as MantineColorPicker, Group } from '@mantine/core'; -import { useState, useCallback, useEffect } from 'react'; -import ColorizeIcon from '@mui/icons-material/Colorize'; +import { ActionIcon, Tooltip, Popover, Stack, ColorSwatch, ColorPicker as MantineColorPicker, Group } from "@mantine/core"; +import { useState, useCallback, useEffect } from "react"; +import ColorizeIcon from "@mui/icons-material/Colorize"; // safari and firefox do not support the eye dropper API, only edge, chrome and opera do. // the button is hidden in the UI if the API is not supported. -const supportsEyeDropper = typeof window !== 'undefined' && 'EyeDropper' in window; +const supportsEyeDropper = typeof window !== "undefined" && "EyeDropper" in window; interface EyeDropper { open(): Promise<{ sRGBHex: string }>; } -declare const EyeDropper: { new(): EyeDropper }; +declare const EyeDropper: { new (): EyeDropper }; interface ColorControlProps { value: string; @@ -24,7 +24,9 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color // Only propagate to the parent (which triggers expensive annotation updates) // on onChangeEnd (mouse-up / swatch click), preventing infinite re-render loops. const [localColor, setLocalColor] = useState(value); - useEffect(() => { setLocalColor(value); }, [value]); + useEffect(() => { + setLocalColor(value); + }, [value]); const handleEyeDropper = useCallback(async () => { if (!supportsEyeDropper) return; @@ -50,13 +52,13 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color styles={{ root: { flexShrink: 0, - backgroundColor: 'var(--bg-raised)', - border: '1px solid var(--border-default)', - color: 'var(--text-secondary)', - '&:hover': { - backgroundColor: 'var(--hover-bg)', - borderColor: 'var(--border-strong)', - color: 'var(--text-primary)', + backgroundColor: "var(--bg-raised)", + border: "1px solid var(--border-default)", + color: "var(--text-secondary)", + "&:hover": { + backgroundColor: "var(--hover-bg)", + borderColor: "var(--border-strong)", + color: "var(--text-primary)", }, }, }} @@ -73,8 +75,16 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color onChange={setLocalColor} onChangeEnd={onChange} swatches={[ - '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', - '#ffff00', '#ff00ff', '#00ffff', '#ffa500', 'transparent' + "#000000", + "#ffffff", + "#ff0000", + "#00ff00", + "#0000ff", + "#ffff00", + "#ff00ff", + "#00ffff", + "#ffa500", + "transparent", ]} swatchesPerRow={5} size="sm" @@ -82,7 +92,13 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color {supportsEyeDropper && ( - + diff --git a/frontend/src/core/components/annotation/shared/ColorPicker.tsx b/frontend/src/core/components/annotation/shared/ColorPicker.tsx index 21656b1f23..71f326a7d5 100644 --- a/frontend/src/core/components/annotation/shared/ColorPicker.tsx +++ b/frontend/src/core/components/annotation/shared/ColorPicker.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch, Slider, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; +import React from "react"; +import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch, Slider, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; interface ColorPickerProps { isOpen: boolean; @@ -26,48 +26,42 @@ export const ColorPicker: React.FC = ({ opacityLabel, }) => { const { t } = useTranslation(); - const resolvedTitle = title ?? t('colorPicker.title', 'Choose colour'); - const resolvedOpacityLabel = opacityLabel ?? t('annotation.opacity', 'Opacity'); + const resolvedTitle = title ?? t("colorPicker.title", "Choose colour"); + const resolvedOpacityLabel = opacityLabel ?? t("annotation.opacity", "Opacity"); return ( - + {showOpacity && onOpacityChange && opacity !== undefined && ( - {resolvedOpacityLabel} + + {resolvedOpacityLabel} + )} - + @@ -80,18 +74,6 @@ interface ColorSwatchButtonProps { size?: number; } -export const ColorSwatchButton: React.FC = ({ - color, - onClick, - size = 24 -}) => { - return ( - - ); +export const ColorSwatchButton: React.FC = ({ color, onClick, size = 24 }) => { + return ; }; diff --git a/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx b/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx index fd8864be26..080a05c83a 100644 --- a/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx +++ b/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Paper, Button, Modal, Stack, Text, Group } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker'; -import PenSizeSelector from '@app/components/tools/sign/PenSizeSelector'; -import SignaturePad from 'signature_pad'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; +import React, { useEffect, useRef, useState } from "react"; +import { Paper, Button, Modal, Stack, Text, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ColorSwatchButton } from "@app/components/annotation/shared/ColorPicker"; +import PenSizeSelector from "@app/components/tools/sign/PenSizeSelector"; +import SignaturePad from "signature_pad"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; interface DrawingCanvasProps { selectedColor: string; @@ -68,7 +68,7 @@ export const DrawingCanvas: React.FC = ({ if (savedSignatureData) { const img = new Image(); img.onload = () => { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (ctx) { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); } @@ -92,13 +92,16 @@ export const DrawingCanvas: React.FC = ({ }, [autoOpen]); const trimCanvas = (canvas: HTMLCanvasElement): string => { - const ctx = canvas.getContext('2d'); - if (!ctx) return canvas.toDataURL('image/png'); + const ctx = canvas.getContext("2d"); + if (!ctx) return canvas.toDataURL("image/png"); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; - let minX = canvas.width, minY = canvas.height, maxX = 0, maxY = 0; + let minX = canvas.width, + minY = canvas.height, + maxX = 0, + maxY = 0; // Find bounds of non-transparent pixels for (let y = 0; y < canvas.height; y++) { @@ -117,21 +120,21 @@ export const DrawingCanvas: React.FC = ({ const trimHeight = maxY - minY + 1; // Create trimmed canvas - const trimmedCanvas = document.createElement('canvas'); + const trimmedCanvas = document.createElement("canvas"); trimmedCanvas.width = trimWidth; trimmedCanvas.height = trimHeight; - const trimmedCtx = trimmedCanvas.getContext('2d'); + const trimmedCtx = trimmedCanvas.getContext("2d"); if (trimmedCtx) { trimmedCtx.drawImage(canvas, minX, minY, trimWidth, trimHeight, 0, 0, trimWidth, trimHeight); } - return trimmedCanvas.toDataURL('image/png'); + return trimmedCanvas.toDataURL("image/png"); }; const renderPreview = (dataUrl: string) => { const canvas = previewCanvasRef.current; if (!canvas) return; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (!ctx) return; const img = new Image(); @@ -153,7 +156,7 @@ export const DrawingCanvas: React.FC = ({ const canvas = modalCanvasRef.current; if (canvas) { const trimmedPng = trimCanvas(canvas); - const untrimmedPng = canvas.toDataURL('image/png'); + const untrimmedPng = canvas.toDataURL("image/png"); setSavedSignatureData(untrimmedPng); // Save untrimmed for restoration onSignatureDataChange(trimmedPng); renderPreview(trimmedPng); @@ -176,7 +179,7 @@ export const DrawingCanvas: React.FC = ({ padRef.current.clear(); } if (previewCanvasRef.current) { - const ctx = previewCanvasRef.current.getContext('2d'); + const ctx = previewCanvasRef.current.getContext("2d"); if (ctx) { ctx.clearRect(0, 0, previewCanvasRef.current.width, previewCanvasRef.current.height); } @@ -209,7 +212,7 @@ export const DrawingCanvas: React.FC = ({ useEffect(() => { const canvas = previewCanvasRef.current; if (!canvas) return; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (!ctx) return; if (!initialSignatureData) { @@ -227,42 +230,45 @@ export const DrawingCanvas: React.FC = ({ - {t('sign.canvas.heading', 'Draw your signature')} - + {t("sign.canvas.heading", "Draw your signature")} + - {t('sign.canvas.clickToOpen', 'Click to open the drawing canvas')} + {t("sign.canvas.clickToOpen", "Click to open the drawing canvas")} - + - {t('sign.canvas.colorLabel', 'Colour')} + {t("sign.canvas.colorLabel", "Colour")} - + - {t('sign.canvas.penSizeLabel', 'Pen size')} + {t("sign.canvas.penSizeLabel", "Pen size")} = ({ updatePenSize(size); }} onInputChange={onPenSizeInputChange} - placeholder={t('sign.canvas.penSizePlaceholder', 'Size')} + placeholder={t("sign.canvas.penSizePlaceholder", "Size")} size="compact-sm" - style={{ width: '80px' }} + style={{ width: "80px" }} /> @@ -286,26 +292,24 @@ export const DrawingCanvas: React.FC = ({ if (el) initPad(el); }} style={{ - border: '1px solid #ccc', - borderRadius: '4px', - display: 'block', - touchAction: 'none', - backgroundColor: 'white', - width: '100%', - maxWidth: '50rem', - height: '25rem', - cursor: 'crosshair', + border: "1px solid #ccc", + borderRadius: "4px", + display: "block", + touchAction: "none", + backgroundColor: "white", + width: "100%", + maxWidth: "50rem", + height: "25rem", + cursor: "crosshair", }} /> -
+
- +
diff --git a/frontend/src/core/components/annotation/shared/DrawingControls.tsx b/frontend/src/core/components/annotation/shared/DrawingControls.tsx index 3c28a594e0..9de5e45b9a 100644 --- a/frontend/src/core/components/annotation/shared/DrawingControls.tsx +++ b/frontend/src/core/components/annotation/shared/DrawingControls.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Group, Button, ActionIcon, Tooltip } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { LocalIcon } from '@app/components/shared/LocalIcon'; +import React from "react"; +import { Group, Button, ActionIcon, Tooltip } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { LocalIcon } from "@app/components/shared/LocalIcon"; interface DrawingControlsProps { onUndo?: () => void; @@ -35,30 +35,30 @@ export const DrawingControls: React.FC = ({ return ( {onUndo && ( - + - + )} {onRedo && ( - + - + )} @@ -67,13 +67,7 @@ export const DrawingControls: React.FC = ({ {/* Place Signature Button */} {showPlaceButton && onPlaceSignature && ( - )} diff --git a/frontend/src/core/components/annotation/shared/ImageUploader.tsx b/frontend/src/core/components/annotation/shared/ImageUploader.tsx index ee2cdd123f..86bbbe7b96 100644 --- a/frontend/src/core/components/annotation/shared/ImageUploader.tsx +++ b/frontend/src/core/components/annotation/shared/ImageUploader.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; -import { FileInput, Text, Stack, Checkbox } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { removeWhiteBackground } from '@app/utils/imageTransparency'; -import { alert } from '@app/components/toast'; +import React, { useState } from "react"; +import { FileInput, Text, Stack, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { removeWhiteBackground } from "@app/utils/imageTransparency"; +import { alert } from "@app/components/toast"; interface ImageUploaderProps { onImageChange: (file: File | null) => void; @@ -22,7 +22,7 @@ export const ImageUploader: React.FC = ({ placeholder, hint, allowBackgroundRemoval = false, - onProcessedImageData + onProcessedImageData, }) => { const { t } = useTranslation(); const [removeBackground, setRemoveBackground] = useState(false); @@ -36,15 +36,18 @@ export const ImageUploader: React.FC = ({ try { const transparentImageDataUrl = await removeWhiteBackground(imageSource, { autoDetectCorner: true, - tolerance: 15 + tolerance: 15, }); onProcessedImageData?.(transparentImageDataUrl); } catch (error) { - console.error('Error removing background:', error); + console.error("Error removing background:", error); alert({ - title: t('sign.image.backgroundRemovalFailedTitle', 'Background removal failed'), - body: t('sign.image.backgroundRemovalFailedMessage', 'Could not remove the background from the image. Using original image instead.'), - alertType: 'error' + title: t("sign.image.backgroundRemovalFailedTitle", "Background removal failed"), + body: t( + "sign.image.backgroundRemovalFailedMessage", + "Could not remove the background from the image. Using original image instead.", + ), + alertType: "error", }); onProcessedImageData?.(null); } finally { @@ -52,7 +55,7 @@ export const ImageUploader: React.FC = ({ } } else { // When background removal is disabled, return the original image data - if (typeof imageSource === 'string') { + if (typeof imageSource === "string") { onProcessedImageData?.(imageSource); } else { // Convert File to data URL if needed @@ -69,8 +72,8 @@ export const ImageUploader: React.FC = ({ if (file && !disabled) { try { // Validate that it's actually an image file or SVG - if (!file.type.startsWith('image/') && !file.name.toLowerCase().endsWith('.svg')) { - console.error('Selected file is not an image or SVG'); + if (!file.type.startsWith("image/") && !file.name.toLowerCase().endsWith(".svg")) { + console.error("Selected file is not an image or SVG"); return; } @@ -78,10 +81,10 @@ export const ImageUploader: React.FC = ({ onImageChange(file); let dataUrlToProcess: string; - + // Check if file is SVG - const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg'); - + const isSvg = file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); + if (isSvg) { // For SVG, convert to PNG so it can be embedded in PDF dataUrlToProcess = await convertSvgToPng(file); @@ -98,7 +101,7 @@ export const ImageUploader: React.FC = ({ setOriginalImageData(dataUrlToProcess); await processImage(dataUrlToProcess, removeBackground); } catch (error) { - console.error('Error processing image file:', error); + console.error("Error processing image file:", error); } } else if (!file) { // Clear image data when no file is selected @@ -116,33 +119,33 @@ export const ImageUploader: React.FC = ({ reader.onload = async (e) => { try { const svgText = e.target?.result as string; - + // Parse SVG to get dimensions const parser = new DOMParser(); - const svgDoc = parser.parseFromString(svgText, 'image/svg+xml'); + const svgDoc = parser.parseFromString(svgText, "image/svg+xml"); const svgElement = svgDoc.documentElement; - + // Get SVG dimensions - let width = 800; // Default width + let width = 800; // Default width let height = 600; // Default height - - if (svgElement.hasAttribute('width') && svgElement.hasAttribute('height')) { - width = parseFloat(svgElement.getAttribute('width') || '800'); - height = parseFloat(svgElement.getAttribute('height') || '600'); - } else if (svgElement.hasAttribute('viewBox')) { - const viewBox = svgElement.getAttribute('viewBox')?.split(/\s+|,/); + + if (svgElement.hasAttribute("width") && svgElement.hasAttribute("height")) { + width = parseFloat(svgElement.getAttribute("width") || "800"); + height = parseFloat(svgElement.getAttribute("height") || "600"); + } else if (svgElement.hasAttribute("viewBox")) { + const viewBox = svgElement.getAttribute("viewBox")?.split(/\s+|,/); if (viewBox && viewBox.length === 4) { width = parseFloat(viewBox[2]); height = parseFloat(viewBox[3]); } } - + // Ensure reasonable dimensions if (width === 0 || height === 0 || !isFinite(width) || !isFinite(height)) { width = 800; height = 600; } - + // Scale large SVGs down const maxDimension = 2048; if (width > maxDimension || height > maxDimension) { @@ -150,68 +153,73 @@ export const ImageUploader: React.FC = ({ width *= scale; height *= scale; } - - console.log('Converting SVG to PNG:', { width, height }); - + + console.log("Converting SVG to PNG:", { width, height }); + // Create an image element to render SVG const img = new Image(); - const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' }); + const blob = new Blob([svgText], { type: "image/svg+xml;charset=utf-8" }); const url = URL.createObjectURL(blob); - + img.onload = () => { try { // Use computed dimensions or image natural dimensions const finalWidth = img.naturalWidth || img.width || width; const finalHeight = img.naturalHeight || img.height || height; - - console.log('Image loaded:', { naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight, finalWidth, finalHeight }); - + + console.log("Image loaded:", { + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + finalWidth, + finalHeight, + }); + // Create canvas to convert to PNG - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = finalWidth; canvas.height = finalHeight; - - const ctx = canvas.getContext('2d'); + + const ctx = canvas.getContext("2d"); if (!ctx) { URL.revokeObjectURL(url); - reject(new Error('Failed to get canvas context')); + reject(new Error("Failed to get canvas context")); return; } - + // Fill with white background (optional, for transparency support) - ctx.fillStyle = 'white'; + ctx.fillStyle = "white"; ctx.fillRect(0, 0, finalWidth, finalHeight); - + // Draw SVG ctx.drawImage(img, 0, 0, finalWidth, finalHeight); URL.revokeObjectURL(url); - + // Convert canvas to PNG data URL - const pngDataUrl = canvas.toDataURL('image/png'); - console.log('SVG converted to PNG successfully'); + const pngDataUrl = canvas.toDataURL("image/png"); + console.log("SVG converted to PNG successfully"); resolve(pngDataUrl); } catch (error) { URL.revokeObjectURL(url); - console.error('Error during canvas rendering:', error); + console.error("Error during canvas rendering:", error); reject(error); } }; - + img.onerror = (error) => { URL.revokeObjectURL(url); - console.error('Failed to load SVG image:', error); - reject(new Error('Failed to load SVG image')); + console.error("Failed to load SVG image:", error); + reject(new Error("Failed to load SVG image")); }; - + img.src = url; } catch (error) { - console.error('Error parsing SVG:', error); + console.error("Error parsing SVG:", error); reject(error); } }; - + reader.onerror = () => { - console.error('Error reading file:', reader.error); + console.error("Error reading file:", reader.error); reject(reader.error); }; reader.readAsText(file); @@ -231,7 +239,7 @@ export const ImageUploader: React.FC = ({ = ({ {allowBackgroundRemoval && ( handleBackgroundRemovalChange(event.currentTarget.checked)} disabled={disabled || !currentFile || isProcessing} @@ -252,7 +260,7 @@ export const ImageUploader: React.FC = ({ )} {isProcessing && ( - {t('sign.image.processing', 'Processing image...')} + {t("sign.image.processing", "Processing image...")} )} diff --git a/frontend/src/core/components/annotation/shared/OpacityControl.tsx b/frontend/src/core/components/annotation/shared/OpacityControl.tsx index 27b1f10dd9..914d4f038d 100644 --- a/frontend/src/core/components/annotation/shared/OpacityControl.tsx +++ b/frontend/src/core/components/annotation/shared/OpacityControl.tsx @@ -1,7 +1,7 @@ -import { ActionIcon, Tooltip, Popover, Stack, Slider, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; -import OpacityIcon from '@mui/icons-material/Opacity'; +import { ActionIcon, Tooltip, Popover, Stack, Slider, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import OpacityIcon from "@mui/icons-material/Opacity"; interface OpacityControlProps { value: number; // 0-100 @@ -16,7 +16,7 @@ export function OpacityControl({ value, onChange, disabled = false }: OpacityCon return ( - + - {t('annotation.opacity', 'Opacity')} + {t("annotation.opacity", "Opacity")} - `${val}%`} - /> + `${val}%`} /> diff --git a/frontend/src/core/components/annotation/shared/PropertiesPopover.tsx b/frontend/src/core/components/annotation/shared/PropertiesPopover.tsx index 2f5d424ab6..8828f3b937 100644 --- a/frontend/src/core/components/annotation/shared/PropertiesPopover.tsx +++ b/frontend/src/core/components/annotation/shared/PropertiesPopover.tsx @@ -1,15 +1,15 @@ -import { ActionIcon, Tooltip, Popover, Stack, Slider, Text, Group, Button } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; -import type { TrackedAnnotation } from '@embedpdf/plugin-annotation'; -import type { PdfAnnotationObject } from '@embedpdf/models'; -import type { AnnotationPatch } from '@app/components/viewer/viewerTypes'; -import TuneIcon from '@mui/icons-material/Tune'; -import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft'; -import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter'; -import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight'; +import { ActionIcon, Tooltip, Popover, Stack, Slider, Text, Group, Button } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import type { TrackedAnnotation } from "@embedpdf/plugin-annotation"; +import type { PdfAnnotationObject } from "@embedpdf/models"; +import type { AnnotationPatch } from "@app/components/viewer/viewerTypes"; +import TuneIcon from "@mui/icons-material/Tune"; +import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft"; +import FormatAlignCenterIcon from "@mui/icons-material/FormatAlignCenter"; +import FormatAlignRightIcon from "@mui/icons-material/FormatAlignRight"; -export type PropertiesAnnotationType = 'text' | 'note' | 'shape'; +export type PropertiesAnnotationType = "text" | "note" | "shape"; interface PropertiesPopoverProps { annotationType: PropertiesAnnotationType; @@ -18,12 +18,7 @@ interface PropertiesPopoverProps { disabled?: boolean; } -export function PropertiesPopover({ - annotationType, - annotation, - onUpdate, - disabled = false, -}: PropertiesPopoverProps) { +export function PropertiesPopover({ annotationType, annotation, onUpdate, disabled = false }: PropertiesPopoverProps) { const { t } = useTranslation(); const [opened, setOpened] = useState(false); @@ -41,17 +36,17 @@ export function PropertiesPopover({ const fontSize = obj?.fontSize ?? 14; const textAlign = obj?.textAlign; const currentAlign = - typeof textAlign === 'number' + typeof textAlign === "number" ? textAlign === 1 - ? 'center' + ? "center" : textAlign === 2 - ? 'right' - : 'left' - : textAlign === 'center' - ? 'center' - : textAlign === 'right' - ? 'right' - : 'left'; + ? "right" + : "left" + : textAlign === "center" + ? "center" + : textAlign === "right" + ? "right" + : "left"; // For shapes const opacity = Math.round((obj?.opacity ?? 1) * 100); @@ -63,7 +58,7 @@ export function PropertiesPopover({ {/* Font Size */}
- {t('annotation.fontSize', 'Font size')} + {t("annotation.fontSize", "Font size")} - {t('annotation.opacity', 'Opacity')} + {t("annotation.opacity", "Opacity")} - {t('annotation.textAlignment', 'Text Alignment')} + {t("annotation.textAlignment", "Text Alignment")} onUpdate({ textAlign: 0 })} size="md" > onUpdate({ textAlign: 1 })} size="md" > onUpdate({ textAlign: 2 })} size="md" > @@ -125,7 +120,7 @@ export function PropertiesPopover({ {/* Opacity */}
- {t('annotation.opacity', 'Opacity')} + {t("annotation.opacity", "Opacity")}
- {t('annotation.strokeWidth', 'Stroke')} + {t("annotation.strokeWidth", "Stroke")}
@@ -188,7 +181,7 @@ export function PropertiesPopover({ return ( - + - {(annotationType === 'text' || annotationType === 'note') && renderTextNoteControls()} - {annotationType === 'shape' && renderShapeControls()} + {(annotationType === "text" || annotationType === "note") && renderTextNoteControls()} + {annotationType === "shape" && renderShapeControls()} ); diff --git a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx index 2385357171..59bcc57564 100644 --- a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx +++ b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from 'react'; -import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box, SegmentedControl } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { ColorPicker } from '@app/components/annotation/shared/ColorPicker'; +import React, { useState, useEffect } from "react"; +import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box, SegmentedControl } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ColorPicker } from "@app/components/annotation/shared/ColorPicker"; interface TextInputWithFontProps { text: string; @@ -12,8 +12,8 @@ interface TextInputWithFontProps { onFontFamilyChange: (family: string) => void; textColor?: string; onTextColorChange?: (color: string) => void; - textAlign?: 'left' | 'center' | 'right'; - onTextAlignChange?: (align: 'left' | 'center' | 'right') => void; + textAlign?: "left" | "center" | "right"; + onTextAlignChange?: (align: "left" | "center" | "right") => void; disabled?: boolean; label: string; placeholder: string; @@ -31,9 +31,9 @@ export const TextInputWithFont: React.FC = ({ onFontSizeChange, fontFamily, onFontFamilyChange, - textColor = '#000000', + textColor = "#000000", onTextColorChange, - textAlign = 'left', + textAlign = "left", onTextAlignChange, disabled = false, label, @@ -42,7 +42,7 @@ export const TextInputWithFont: React.FC = ({ fontSizeLabel, fontSizePlaceholder, colorLabel, - onAnyChange + onAnyChange, }) => { const { t } = useTranslation(); const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString()); @@ -61,14 +61,37 @@ export const TextInputWithFont: React.FC = ({ }, [textColor]); const fontOptions = [ - { value: 'Helvetica', label: 'Helvetica' }, - { value: 'Times-Roman', label: 'Times' }, - { value: 'Courier', label: 'Courier' }, - { value: 'Arial', label: 'Arial' }, - { value: 'Georgia', label: 'Georgia' }, + { value: "Helvetica", label: "Helvetica" }, + { value: "Times-Roman", label: "Times" }, + { value: "Courier", label: "Courier" }, + { value: "Arial", label: "Arial" }, + { value: "Georgia", label: "Georgia" }, ]; - const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48', '56', '64', '72', '80', '96', '112', '128', '144', '160', '176', '192', '200']; + const fontSizeOptions = [ + "8", + "12", + "16", + "20", + "24", + "28", + "32", + "36", + "40", + "48", + "56", + "64", + "72", + "80", + "96", + "112", + "128", + "144", + "160", + "176", + "192", + "200", + ]; // Validate hex color const isValidHexColor = (color: string): boolean => { @@ -94,7 +117,7 @@ export const TextInputWithFont: React.FC = ({ label={fontLabel} value={fontFamily} onChange={(value) => { - onFontFamilyChange(value || 'Helvetica'); + onFontFamilyChange(value || "Helvetica"); onAnyChange?.(); }} data={fontOptions} @@ -187,7 +210,7 @@ export const TextInputWithFont: React.FC = ({ setColorInput(textColor); } }} - style={{ width: '100%' }} + style={{ width: "100%" }} rightSection={ !disabled && setIsColorPickerOpen(true)} @@ -195,9 +218,9 @@ export const TextInputWithFont: React.FC = ({ width: 24, height: 24, backgroundColor: textColor, - border: '1px solid #ccc', + border: "1px solid #ccc", borderRadius: 4, - cursor: disabled ? 'default' : 'pointer' + cursor: disabled ? "default" : "pointer", }} /> } @@ -224,14 +247,14 @@ export const TextInputWithFont: React.FC = ({ { - onTextAlignChange(value as 'left' | 'center' | 'right'); + onTextAlignChange(value as "left" | "center" | "right"); onAnyChange?.(); }} disabled={disabled} data={[ - { label: t('textAlign.left', 'Left'), value: 'left' }, - { label: t('textAlign.center', 'Center'), value: 'center' }, - { label: t('textAlign.right', 'Right'), value: 'right' }, + { label: t("textAlign.left", "Left"), value: "left" }, + { label: t("textAlign.center", "Center"), value: "center" }, + { label: t("textAlign.right", "Right"), value: "right" }, ]} /> )} diff --git a/frontend/src/core/components/annotation/shared/WidthControl.tsx b/frontend/src/core/components/annotation/shared/WidthControl.tsx index b99d35c996..58553e64b8 100644 --- a/frontend/src/core/components/annotation/shared/WidthControl.tsx +++ b/frontend/src/core/components/annotation/shared/WidthControl.tsx @@ -1,7 +1,7 @@ -import { ActionIcon, Tooltip, Popover, Stack, Slider, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; -import LineWeightIcon from '@mui/icons-material/LineWeight'; +import { ActionIcon, Tooltip, Popover, Stack, Slider, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import LineWeightIcon from "@mui/icons-material/LineWeight"; interface WidthControlProps { value: number; @@ -18,7 +18,7 @@ export function WidthControl({ value, onChange, min, max, disabled = false }: Wi return ( - + - {t('annotation.width', 'Width')} + {t("annotation.width", "Width")} - `${val}pt`} - /> + `${val}pt`} /> diff --git a/frontend/src/core/components/annotation/tools/DrawingTool.tsx b/frontend/src/core/components/annotation/tools/DrawingTool.tsx index f3643de35c..2699418eed 100644 --- a/frontend/src/core/components/annotation/tools/DrawingTool.tsx +++ b/frontend/src/core/components/annotation/tools/DrawingTool.tsx @@ -1,33 +1,26 @@ -import React, { useState } from 'react'; -import { Stack } from '@mantine/core'; -import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool'; -import { DrawingCanvas } from '@app/components/annotation/shared/DrawingCanvas'; +import React, { useState } from "react"; +import { Stack } from "@mantine/core"; +import { BaseAnnotationTool } from "@app/components/annotation/shared/BaseAnnotationTool"; +import { DrawingCanvas } from "@app/components/annotation/shared/DrawingCanvas"; interface DrawingToolProps { onDrawingChange?: (data: string | null) => void; disabled?: boolean; } -export const DrawingTool: React.FC = ({ - onDrawingChange, - disabled = false -}) => { - const [selectedColor] = useState('#000000'); +export const DrawingTool: React.FC = ({ onDrawingChange, disabled = false }) => { + const [selectedColor] = useState("#000000"); const [penSize, setPenSize] = useState(2); - const [penSizeInput, setPenSizeInput] = useState('2'); + const [penSizeInput, setPenSizeInput] = useState("2"); const toolConfig = { enableDrawing: true, showPlaceButton: true, - placeButtonText: "Place Drawing" + placeButtonText: "Place Drawing", }; return ( - + = ({ ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/tools/ImageTool.tsx b/frontend/src/core/components/annotation/tools/ImageTool.tsx index 0704546965..fa59e272e7 100644 --- a/frontend/src/core/components/annotation/tools/ImageTool.tsx +++ b/frontend/src/core/components/annotation/tools/ImageTool.tsx @@ -1,17 +1,14 @@ -import React, { useState } from 'react'; -import { Stack } from '@mantine/core'; -import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool'; -import { ImageUploader } from '@app/components/annotation/shared/ImageUploader'; +import React, { useState } from "react"; +import { Stack } from "@mantine/core"; +import { BaseAnnotationTool } from "@app/components/annotation/shared/BaseAnnotationTool"; +import { ImageUploader } from "@app/components/annotation/shared/ImageUploader"; interface ImageToolProps { onImageChange?: (data: string | null) => void; disabled?: boolean; } -export const ImageTool: React.FC = ({ - onImageChange, - disabled = false -}) => { +export const ImageTool: React.FC = ({ onImageChange, disabled = false }) => { const [, setImageData] = useState(null); const handleImageUpload = async (file: File | null) => { @@ -23,7 +20,7 @@ export const ImageTool: React.FC = ({ if (e.target?.result) { resolve(e.target.result as string); } else { - reject(new Error('Failed to read file')); + reject(new Error("Failed to read file")); } }; reader.onerror = () => reject(reader.error); @@ -33,7 +30,7 @@ export const ImageTool: React.FC = ({ setImageData(result); onImageChange?.(result); } catch (error) { - console.error('Error reading file:', error); + console.error("Error reading file:", error); } } else if (!file) { setImageData(null); @@ -44,15 +41,11 @@ export const ImageTool: React.FC = ({ const toolConfig = { enableImageUpload: true, showPlaceButton: true, - placeButtonText: "Place Image" + placeButtonText: "Place Image", }; return ( - + = ({ ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/fileEditor/AddFileCard.tsx b/frontend/src/core/components/fileEditor/AddFileCard.tsx index c5cafd7561..8e321cc62c 100644 --- a/frontend/src/core/components/fileEditor/AddFileCard.tsx +++ b/frontend/src/core/components/fileEditor/AddFileCard.tsx @@ -1,14 +1,14 @@ -import React, { useRef, useState } from 'react'; -import { Button, Group, useMantineColorScheme } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import AddIcon from '@mui/icons-material/Add'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import styles from '@app/components/fileEditor/FileEditor.module.css'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; -import { openFilesFromDisk } from '@app/services/openFilesFromDisk'; +import React, { useRef, useState } from "react"; +import { Button, Group, useMantineColorScheme } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import AddIcon from "@mui/icons-material/Add"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; +import styles from "@app/components/fileEditor/FileEditor.module.css"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; +import { openFilesFromDisk } from "@app/services/openFilesFromDisk"; interface AddFileCardProps { onFileSelect: (files: File[]) => void; @@ -16,11 +16,7 @@ interface AddFileCardProps { multiple?: boolean; } -const AddFileCard = ({ - onFileSelect, - accept, - multiple = true -}: AddFileCardProps) => { +const AddFileCard = ({ onFileSelect, accept, multiple = true }: AddFileCardProps) => { const { t } = useTranslation(); const fileInputRef = useRef(null); const { openFilesModal } = useFilesModalContext(); @@ -38,7 +34,7 @@ const AddFileCard = ({ e.stopPropagation(); const files = await openFilesFromDisk({ multiple, - onFallbackOpen: () => fileInputRef.current?.click() + onFallbackOpen: () => fileInputRef.current?.click(), }); if (files.length > 0) { onFileSelect(files); @@ -56,7 +52,7 @@ const AddFileCard = ({ onFileSelect(files); } // Reset input so same files can be selected again - event.target.value = ''; + event.target.value = ""; }; return ( @@ -67,17 +63,17 @@ const AddFileCard = ({ accept={accept} multiple={multiple} onChange={handleFileChange} - style={{ display: 'none' }} + style={{ display: "none" }} />
{ - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleCardClick(); } @@ -86,11 +82,9 @@ const AddFileCard = ({ {/* Header bar - matches FileEditorThumbnail structure */}
- -
-
- {t('fileEditor.addFiles', 'Add Files')} +
+
{t("fileEditor.addFiles", "Add Files")}
@@ -99,84 +93,81 @@ const AddFileCard = ({ {/* Stirling PDF Branding */} Stirling PDF {/* Add Files + Native Upload Buttons - styled like LandingPage */}
setIsUploadHover(false)} >
{/* Instruction Text */} {terminology.dropFilesHere} diff --git a/frontend/src/core/components/fileEditor/FileEditor.module.css b/frontend/src/core/components/fileEditor/FileEditor.module.css index 4f26c8bce5..43ef0d1af9 100644 --- a/frontend/src/core/components/fileEditor/FileEditor.module.css +++ b/frontend/src/core/components/fileEditor/FileEditor.module.css @@ -6,7 +6,10 @@ background: var(--file-card-bg); border-radius: 0.0625rem; cursor: pointer; - transition: box-shadow 0.18s ease, outline-color 0.18s ease, transform 0.18s ease; + transition: + box-shadow 0.18s ease, + outline-color 0.18s ease, + transform 0.18s ease; max-width: 100%; max-height: 100%; overflow: visible; @@ -45,8 +48,8 @@ } .headerResting { - background: #3B4B6E; /* dark blue for unselected in light mode */ - color: #FFFFFF; + background: #3b4b6e; /* dark blue for unselected in light mode */ + color: #ffffff; border-bottom: 1px solid var(--border-default); } @@ -66,7 +69,7 @@ /* Unsupported (but not errored) header appearance */ .headerUnsupported { background: var(--unsupported-bar-bg); /* neutral gray */ - color: #FFFFFF; + color: #ffffff; border-bottom: 1px solid var(--unsupported-bar-border); } @@ -103,7 +106,7 @@ } .headerIconButton { - color: #FFFFFF !important; + color: #ffffff !important; } /* Menu dropdown */ @@ -226,14 +229,13 @@ } .pinned { - color: #FFC107 !important; + color: #ffc107 !important; } - /* Unsupported file indicator */ .unsupportedPill { margin-left: 1.75rem; - background: #6B7280; + background: #6b7280; color: white; padding: 4px 8px; border-radius: 12px; @@ -264,7 +266,8 @@ /* Animations */ @keyframes pulse { - 0%, 100% { + 0%, + 100% { opacity: 1; } 50% { @@ -288,15 +291,15 @@ DARK MODE OVERRIDES ========================= */ :global([data-mantine-color-scheme="dark"]) .card { - outline-color: #3A4047; /* deselected stroke */ + outline-color: #3a4047; /* deselected stroke */ } :global([data-mantine-color-scheme="dark"]) .card[data-selected="true"] { - outline-color: #4B525A; /* selected stroke (subtle grey) */ + outline-color: #4b525a; /* selected stroke (subtle grey) */ } :global([data-mantine-color-scheme="dark"]) .headerResting { - background: #1F2329; /* requested default unselected color */ + background: #1f2329; /* requested default unselected color */ color: var(--tool-header-text); /* #D0D6DC */ border-bottom-color: var(--tool-header-border); /* #3A4047 */ } @@ -308,16 +311,16 @@ } :global([data-mantine-color-scheme="dark"]) .title { - color: #D0D6DC; /* title text */ + color: #d0d6dc; /* title text */ } :global([data-mantine-color-scheme="dark"]) .meta { - color: #6B7280; /* subtitle text */ + color: #6b7280; /* subtitle text */ } /* Light mode selected header stroke override */ :global([data-mantine-color-scheme="light"]) .card[data-selected="true"] { - outline-color: #3B4B6E; + outline-color: #3b4b6e; } /* ========================= diff --git a/frontend/src/core/components/fileEditor/FileEditor.tsx b/frontend/src/core/components/fileEditor/FileEditor.tsx index c230495e1a..64a64b6c46 100644 --- a/frontend/src/core/components/fileEditor/FileEditor.tsx +++ b/frontend/src/core/components/fileEditor/FileEditor.tsx @@ -1,22 +1,19 @@ -import { useState, useCallback, useRef, useMemo, useEffect } from 'react'; -import { - Text, Center, Box, LoadingOverlay, Stack -} from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import { useFileSelection, useFileState, useFileManagement, useFileActions, useFileContext } from '@app/contexts/FileContext'; -import { useNavigationActions } from '@app/contexts/NavigationContext'; -import { useViewer } from '@app/contexts/ViewerContext'; -import { zipFileService } from '@app/services/zipFileService'; -import { detectFileExtension } from '@app/utils/fileUtils'; -import FileEditorThumbnail from '@app/components/fileEditor/FileEditorThumbnail'; -import AddFileCard from '@app/components/fileEditor/AddFileCard'; -import FilePickerModal from '@app/components/shared/FilePickerModal'; -import { FileId, StirlingFile } from '@app/types/fileContext'; -import { alert } from '@app/components/toast'; -import { downloadFile } from '@app/services/downloadService'; -import { useFileEditorRightRailButtons } from '@app/components/fileEditor/fileEditorRightRailButtons'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; - +import { useState, useCallback, useRef, useMemo, useEffect } from "react"; +import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import { useFileSelection, useFileState, useFileManagement, useFileActions, useFileContext } from "@app/contexts/FileContext"; +import { useNavigationActions } from "@app/contexts/NavigationContext"; +import { useViewer } from "@app/contexts/ViewerContext"; +import { zipFileService } from "@app/services/zipFileService"; +import { detectFileExtension } from "@app/utils/fileUtils"; +import FileEditorThumbnail from "@app/components/fileEditor/FileEditorThumbnail"; +import AddFileCard from "@app/components/fileEditor/AddFileCard"; +import FilePickerModal from "@app/components/shared/FilePickerModal"; +import { FileId, StirlingFile } from "@app/types/fileContext"; +import { alert } from "@app/components/toast"; +import { downloadFile } from "@app/services/downloadService"; +import { useFileEditorRightRailButtons } from "@app/components/fileEditor/fileEditorRightRailButtons"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; interface FileEditorProps { onOpenPageEditor?: () => void; @@ -25,16 +22,15 @@ interface FileEditorProps { supportedExtensions?: string[]; } -const FileEditor = ({ - toolMode = false, - supportedExtensions = ["pdf"] -}: FileEditorProps) => { - +const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEditorProps) => { // Utility function to check if a file extension is supported - const isFileSupported = useCallback((fileName: string): boolean => { - const extension = detectFileExtension(fileName); - return extension ? supportedExtensions.includes(extension) : false; - }, [supportedExtensions]); + const isFileSupported = useCallback( + (fileName: string): boolean => { + const extension = detectFileExtension(fileName); + return extension ? supportedExtensions.includes(extension) : false; + }, + [supportedExtensions], + ); // Use optimized FileContext hooks const { state, selectors } = useFileState(); @@ -62,11 +58,11 @@ const FileEditor = ({ const [_error, _setError] = useState(null); // Toast helpers - const showStatus = useCallback((message: string, type: 'neutral' | 'success' | 'warning' | 'error' = 'neutral') => { + const showStatus = useCallback((message: string, type: "neutral" | "success" | "warning" | "error" = "neutral") => { alert({ alertType: type, title: message, expandable: false, durationMs: 4000 }); }, []); const showError = useCallback((message: string) => { - alert({ alertType: 'error', title: 'Error', body: message, expandable: true }); + alert({ alertType: "error", title: "Error", body: message, expandable: true }); }, []); const [selectionMode, setSelectionMode] = useState(toolMode); @@ -76,7 +72,7 @@ const FileEditor = ({ // Compute effective max allowed files based on the active tool and mode const maxAllowed = useMemo(() => { const rawMax = selectedTool?.maxFiles; - return (!toolMode || rawMax == null || rawMax < 0) ? Infinity : rawMax; + return !toolMode || rawMax == null || rawMax < 0 ? Infinity : rawMax; }, [selectedTool?.maxFiles, toolMode]); // Enable selection mode automatically in tool mode @@ -104,8 +100,8 @@ const FileEditor = ({ try { clearAllFileErrors(); } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.warn('Failed to clear file errors on select all:', error); + if (process.env.NODE_ENV === "development") { + console.warn("Failed to clear file errors on select all:", error); } } }, [state.files.ids, setSelectedFiles, clearAllFileErrors, maxAllowed]); @@ -115,8 +111,8 @@ const FileEditor = ({ try { clearAllFileErrors(); } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.warn('Failed to clear file errors on deselect:', error); + if (process.env.NODE_ENV === "development") { + console.warn("Failed to clear file errors on deselect:", error); } } }, [setSelectedFiles, clearAllFileErrors]); @@ -137,69 +133,75 @@ const FileEditor = ({ // Process uploaded files using context // ZIP extraction is now handled automatically in FileContext based on user preferences - const handleFileUpload = useCallback(async (uploadedFiles: File[]) => { - _setError(null); + const handleFileUpload = useCallback( + async (uploadedFiles: File[]) => { + _setError(null); - try { - if (uploadedFiles.length > 0) { - // FileContext will automatically handle ZIP extraction based on user preferences - // - Respects autoUnzip setting - // - Respects autoUnzipFileLimit - // - HTML ZIPs stay intact - // - Non-ZIP files pass through unchanged - await addFiles(uploadedFiles, { selectFiles: true }); - // After auto-selection, enforce maxAllowed if needed - if (Number.isFinite(maxAllowed)) { - const nowSelectedIds = selectors.getSelectedStirlingFileStubs().map(r => r.id); - if (nowSelectedIds.length > maxAllowed) { - setSelectedFiles(nowSelectedIds.slice(-maxAllowed)); + try { + if (uploadedFiles.length > 0) { + // FileContext will automatically handle ZIP extraction based on user preferences + // - Respects autoUnzip setting + // - Respects autoUnzipFileLimit + // - HTML ZIPs stay intact + // - Non-ZIP files pass through unchanged + await addFiles(uploadedFiles, { selectFiles: true }); + // After auto-selection, enforce maxAllowed if needed + if (Number.isFinite(maxAllowed)) { + const nowSelectedIds = selectors.getSelectedStirlingFileStubs().map((r) => r.id); + if (nowSelectedIds.length > maxAllowed) { + setSelectedFiles(nowSelectedIds.slice(-maxAllowed)); + } + } + showStatus(`Added ${uploadedFiles.length} file(s)`, "success"); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to process files"; + showError(errorMessage); + console.error("File processing error:", err); + } + }, + [addFiles, showStatus, showError, selectors, maxAllowed, setSelectedFiles], + ); + + const toggleFile = useCallback( + (fileId: FileId) => { + const currentSelectedIds = contextSelectedIdsRef.current; + + const targetRecord = activeStirlingFileStubs.find((r) => r.id === fileId); + if (!targetRecord) return; + + const contextFileId = fileId; // No need to create a new ID + const isSelected = currentSelectedIds.includes(contextFileId); + + let newSelection: FileId[]; + + if (isSelected) { + // Remove file from selection + newSelection = currentSelectedIds.filter((id) => id !== contextFileId); + } else { + // Add file to selection + // Determine max files allowed from the active tool (negative or undefined means unlimited) + const rawMax = selectedTool?.maxFiles; + const maxAllowed = !toolMode || rawMax == null || rawMax < 0 ? Infinity : rawMax; + + if (maxAllowed === 1) { + // Only one file allowed -> replace selection with the new file + newSelection = [contextFileId]; + } else { + // If at capacity, drop the oldest selected and append the new one + if (Number.isFinite(maxAllowed) && currentSelectedIds.length >= maxAllowed) { + newSelection = [...currentSelectedIds.slice(1), contextFileId]; + } else { + newSelection = [...currentSelectedIds, contextFileId]; } } - showStatus(`Added ${uploadedFiles.length} file(s)`, 'success'); } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to process files'; - showError(errorMessage); - console.error('File processing error:', err); - } - }, [addFiles, showStatus, showError, selectors, maxAllowed, setSelectedFiles]); - const toggleFile = useCallback((fileId: FileId) => { - const currentSelectedIds = contextSelectedIdsRef.current; - - const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId); - if (!targetRecord) return; - - const contextFileId = fileId; // No need to create a new ID - const isSelected = currentSelectedIds.includes(contextFileId); - - let newSelection: FileId[]; - - if (isSelected) { - // Remove file from selection - newSelection = currentSelectedIds.filter(id => id !== contextFileId); - } else { - // Add file to selection - // Determine max files allowed from the active tool (negative or undefined means unlimited) - const rawMax = selectedTool?.maxFiles; - const maxAllowed = (!toolMode || rawMax == null || rawMax < 0) ? Infinity : rawMax; - - if (maxAllowed === 1) { - // Only one file allowed -> replace selection with the new file - newSelection = [contextFileId]; - } else { - // If at capacity, drop the oldest selected and append the new one - if (Number.isFinite(maxAllowed) && currentSelectedIds.length >= maxAllowed) { - newSelection = [...currentSelectedIds.slice(1), contextFileId]; - } else { - newSelection = [...currentSelectedIds, contextFileId]; - } - } - } - - // Update context (this automatically updates tool selection since they use the same action) - setSelectedFiles(newSelection); - }, [setSelectedFiles, toolMode, _setStatus, activeStirlingFileStubs, selectedTool?.maxFiles]); + // Update context (this automatically updates tool selection since they use the same action) + setSelectedFiles(newSelection); + }, + [setSelectedFiles, toolMode, _setStatus, activeStirlingFileStubs, selectedTool?.maxFiles], + ); // Enforce maxAllowed when tool changes or when an external action sets too many selected files useEffect(() => { @@ -208,154 +210,174 @@ const FileEditor = ({ } }, [maxAllowed, selectedFileIds, setSelectedFiles]); - // File reordering handler for drag and drop - const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => { - const currentIds = activeStirlingFileStubs.map(r => r.id); + const handleReorderFiles = useCallback( + (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => { + const currentIds = activeStirlingFileStubs.map((r) => r.id); - // Find indices - const sourceIndex = currentIds.findIndex(id => id === sourceFileId); - const targetIndex = currentIds.findIndex(id => id === targetFileId); + // Find indices + const sourceIndex = currentIds.findIndex((id) => id === sourceFileId); + const targetIndex = currentIds.findIndex((id) => id === targetFileId); - if (sourceIndex === -1 || targetIndex === -1) { - console.warn('Could not find source or target file for reordering'); - return; - } - - // Handle multi-file selection reordering - const filesToMove = selectedFileIds.length > 1 - ? selectedFileIds.filter(id => currentIds.includes(id)) - : [sourceFileId]; - - // Create new order - const newOrder = [...currentIds]; - - // Remove files to move from their current positions (in reverse order to maintain indices) - const sourceIndices = filesToMove.map(id => newOrder.findIndex(nId => nId === id)) - .sort((a, b) => b - a); // Sort descending - - sourceIndices.forEach(index => { - newOrder.splice(index, 1); - }); - - // Calculate insertion index after removals - let insertIndex = newOrder.findIndex(id => id === targetFileId); - if (insertIndex !== -1) { - // Determine if moving forward or backward - const isMovingForward = sourceIndex < targetIndex; - if (isMovingForward) { - // Moving forward: insert after target - insertIndex += 1; - } else { - // Moving backward: insert before target (insertIndex already correct) + if (sourceIndex === -1 || targetIndex === -1) { + console.warn("Could not find source or target file for reordering"); + return; } - } else { - // Target was moved, insert at end - insertIndex = newOrder.length; - } - // Insert files at the calculated position - newOrder.splice(insertIndex, 0, ...filesToMove); + // Handle multi-file selection reordering + const filesToMove = + selectedFileIds.length > 1 ? selectedFileIds.filter((id) => currentIds.includes(id)) : [sourceFileId]; - // Update file order - reorderFiles(newOrder); + // Create new order + const newOrder = [...currentIds]; - // Update status - const moveCount = filesToMove.length; - showStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); - }, [activeStirlingFileStubs, reorderFiles, _setStatus]); + // Remove files to move from their current positions (in reverse order to maintain indices) + const sourceIndices = filesToMove.map((id) => newOrder.findIndex((nId) => nId === id)).sort((a, b) => b - a); // Sort descending + sourceIndices.forEach((index) => { + newOrder.splice(index, 1); + }); + // Calculate insertion index after removals + let insertIndex = newOrder.findIndex((id) => id === targetFileId); + if (insertIndex !== -1) { + // Determine if moving forward or backward + const isMovingForward = sourceIndex < targetIndex; + if (isMovingForward) { + // Moving forward: insert after target + insertIndex += 1; + } else { + // Moving backward: insert before target (insertIndex already correct) + } + } else { + // Target was moved, insert at end + insertIndex = newOrder.length; + } + + // Insert files at the calculated position + newOrder.splice(insertIndex, 0, ...filesToMove); + + // Update file order + reorderFiles(newOrder); + + // Update status + const moveCount = filesToMove.length; + showStatus(`${moveCount > 1 ? `${moveCount} files` : "File"} reordered`); + }, + [activeStirlingFileStubs, reorderFiles, _setStatus], + ); // File operations using context - const handleCloseFile = useCallback((fileId: FileId) => { - const record = activeStirlingFileStubs.find(r => r.id === fileId); - const file = record ? selectors.getFile(record.id) : null; - if (record && file) { - // Remove file from context but keep in storage (close, don't delete) - const contextFileId = record.id; - removeFiles([contextFileId], false); + const handleCloseFile = useCallback( + (fileId: FileId) => { + const record = activeStirlingFileStubs.find((r) => r.id === fileId); + const file = record ? selectors.getFile(record.id) : null; + if (record && file) { + // Remove file from context but keep in storage (close, don't delete) + const contextFileId = record.id; + removeFiles([contextFileId], false); - // Remove from context selections - const currentSelected = selectedFileIds.filter(id => id !== contextFileId); - setSelectedFiles(currentSelected); - } - }, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]); - - const handleDownloadFile = useCallback(async (fileId: FileId) => { - const record = activeStirlingFileStubs.find(r => r.id === fileId); - const file = record ? selectors.getFile(record.id) : null; - console.log('[FileEditor] handleDownloadFile called:', { fileId, hasRecord: !!record, hasFile: !!file, localFilePath: record?.localFilePath, isDirty: record?.isDirty }); - if (record && file) { - const result = await downloadFile({ - data: file, - filename: file.name, - localPath: record.localFilePath - }); - console.log('[FileEditor] Download complete, checking dirty state:', { localFilePath: record.localFilePath, isDirty: record.isDirty, savedPath: result.savedPath }); - // Mark file as clean after successful save to disk - if (result.savedPath) { - console.log('[FileEditor] Marking file as clean:', fileId); - fileActions.updateStirlingFileStub(fileId, { - localFilePath: record.localFilePath ?? result.savedPath, - isDirty: false - }); - } else { - console.log('[FileEditor] Skipping clean mark:', { savedPath: result.savedPath, isDirty: record.isDirty }); + // Remove from context selections + const currentSelected = selectedFileIds.filter((id) => id !== contextFileId); + setSelectedFiles(currentSelected); } - } - }, [activeStirlingFileStubs, selectors, fileActions]); + }, + [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds], + ); - const handleUnzipFile = useCallback(async (fileId: FileId) => { - const record = activeStirlingFileStubs.find(r => r.id === fileId); - const file = record ? selectors.getFile(record.id) : null; - if (record && file) { - try { - // Extract and store files using shared service method - const result = await zipFileService.extractAndStoreFilesWithHistory(file, record); - - if (result.success && result.extractedStubs.length > 0) { - // Add extracted file stubs to FileContext - await fileActions.addStirlingFileStubs(result.extractedStubs); - - // Remove the original ZIP file - removeFiles([fileId], false); - - alert({ - alertType: 'success', - title: `Extracted ${result.extractedStubs.length} file(s) from ${file.name}`, - expandable: false, - durationMs: 3500 + const handleDownloadFile = useCallback( + async (fileId: FileId) => { + const record = activeStirlingFileStubs.find((r) => r.id === fileId); + const file = record ? selectors.getFile(record.id) : null; + console.log("[FileEditor] handleDownloadFile called:", { + fileId, + hasRecord: !!record, + hasFile: !!file, + localFilePath: record?.localFilePath, + isDirty: record?.isDirty, + }); + if (record && file) { + const result = await downloadFile({ + data: file, + filename: file.name, + localPath: record.localFilePath, + }); + console.log("[FileEditor] Download complete, checking dirty state:", { + localFilePath: record.localFilePath, + isDirty: record.isDirty, + savedPath: result.savedPath, + }); + // Mark file as clean after successful save to disk + if (result.savedPath) { + console.log("[FileEditor] Marking file as clean:", fileId); + fileActions.updateStirlingFileStub(fileId, { + localFilePath: record.localFilePath ?? result.savedPath, + isDirty: false, }); } else { + console.log("[FileEditor] Skipping clean mark:", { savedPath: result.savedPath, isDirty: record.isDirty }); + } + } + }, + [activeStirlingFileStubs, selectors, fileActions], + ); + + const handleUnzipFile = useCallback( + async (fileId: FileId) => { + const record = activeStirlingFileStubs.find((r) => r.id === fileId); + const file = record ? selectors.getFile(record.id) : null; + if (record && file) { + try { + // Extract and store files using shared service method + const result = await zipFileService.extractAndStoreFilesWithHistory(file, record); + + if (result.success && result.extractedStubs.length > 0) { + // Add extracted file stubs to FileContext + await fileActions.addStirlingFileStubs(result.extractedStubs); + + // Remove the original ZIP file + removeFiles([fileId], false); + + alert({ + alertType: "success", + title: `Extracted ${result.extractedStubs.length} file(s) from ${file.name}`, + expandable: false, + durationMs: 3500, + }); + } else { + alert({ + alertType: "error", + title: `Failed to extract files from ${file.name}`, + body: result.errors.join("\n"), + expandable: true, + durationMs: 3500, + }); + } + } catch (error) { + console.error("Failed to unzip file:", error); alert({ - alertType: 'error', - title: `Failed to extract files from ${file.name}`, - body: result.errors.join('\n'), - expandable: true, - durationMs: 3500 + alertType: "error", + title: `Error unzipping ${file.name}`, + expandable: false, + durationMs: 3500, }); } - } catch (error) { - console.error('Failed to unzip file:', error); - alert({ - alertType: 'error', - title: `Error unzipping ${file.name}`, - expandable: false, - durationMs: 3500 - }); } - } - }, [activeStirlingFileStubs, selectors, fileActions, removeFiles]); + }, + [activeStirlingFileStubs, selectors, fileActions, removeFiles], + ); - const handleViewFile = useCallback((fileId: FileId) => { - const index = activeStirlingFileStubs.findIndex(r => r.id === fileId); - if (index !== -1) { - setActiveFileId(fileId as string); - setActiveFileIndex(index); - navActions.setWorkbench('viewer'); - } - }, [activeStirlingFileStubs, setActiveFileId, setActiveFileIndex, navActions.setWorkbench]); + const handleViewFile = useCallback( + (fileId: FileId) => { + const index = activeStirlingFileStubs.findIndex((r) => r.id === fileId); + if (index !== -1) { + setActiveFileId(fileId as string); + setActiveFileIndex(index); + navActions.setWorkbench("viewer"); + } + }, + [activeStirlingFileStubs, setActiveFileId, setActiveFileIndex, navActions.setWorkbench], + ); const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => { if (selectedFiles.length === 0) return; @@ -365,91 +387,85 @@ const FileEditor = ({ // The files are already in FileContext, just need to add them to active files showStatus(`Loaded ${selectedFiles.length} files from storage`); } catch (err) { - console.error('Error loading files from storage:', err); - showError('Failed to load some files from storage'); + console.error("Error loading files from storage:", err); + showError("Failed to load some files from storage"); } }, []); - return ( - + + {activeStirlingFileStubs.length === 0 ? ( +
+ + + 📁 + + No files loaded + + Upload PDF files, ZIP archives, or load from storage to get started + + +
+ ) : ( +
+ {/* Add File Card - only show when files exist */} + {activeStirlingFileStubs.length > 0 && } + {activeStirlingFileStubs.map((record, index) => { + return ( + + ); + })} +
+ )} +
- {activeStirlingFileStubs.length === 0 ? ( -
- - 📁 - No files loaded - Upload PDF files, ZIP archives, or load from storage to get started - -
- ) : ( -
- {/* Add File Card - only show when files exist */} - {activeStirlingFileStubs.length > 0 && ( - - )} - - {activeStirlingFileStubs.map((record, index) => { - return ( - - ); - })} -
- )} -
- - {/* File Picker Modal */} - setShowFilePickerModal(false)} - storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent - onSelectFiles={handleLoadFromStorage} - /> - - + {/* File Picker Modal */} + setShowFilePickerModal(false)} + storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent + onSelectFiles={handleLoadFromStorage} + />
); diff --git a/frontend/src/core/components/fileEditor/FileEditorFileName.tsx b/frontend/src/core/components/fileEditor/FileEditorFileName.tsx index a3e4cd4493..82911a9339 100644 --- a/frontend/src/core/components/fileEditor/FileEditorFileName.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorFileName.tsx @@ -1,13 +1,11 @@ -import React from 'react'; -import { StirlingFileStub } from '@app/types/fileContext'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; +import React from "react"; +import { StirlingFileStub } from "@app/types/fileContext"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; interface FileEditorFileNameProps { file: StirlingFileStub; } -const FileEditorFileName = ({ file }: FileEditorFileNameProps) => ( - {file.name} -); +const FileEditorFileName = ({ file }: FileEditorFileNameProps) => {file.name}; export default FileEditorFileName; diff --git a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx index a2973bb7a7..307b498f26 100644 --- a/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -1,38 +1,36 @@ -import React, { useState, useCallback, useRef, useMemo } from 'react'; -import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack, Loader } from '@mantine/core'; -import { useIsMobile } from '@app/hooks/useIsMobile'; -import { alert } from '@app/components/toast'; -import { useTranslation } from 'react-i18next'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; -import CloseIcon from '@mui/icons-material/Close'; -import VisibilityIcon from '@mui/icons-material/Visibility'; -import UnarchiveIcon from '@mui/icons-material/Unarchive'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import LinkIcon from '@mui/icons-material/Link'; -import PushPinIcon from '@mui/icons-material/PushPin'; -import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; -import LockOpenIcon from '@mui/icons-material/LockOpen'; -import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; -import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; -import { StirlingFileStub } from '@app/types/fileContext'; -import { zipFileService } from '@app/services/zipFileService'; - -import styles from '@app/components/fileEditor/FileEditor.module.css'; -import { useFileContext } from '@app/contexts/FileContext'; -import { useFileState } from '@app/contexts/file/fileHooks'; -import { FileId } from '@app/types/file'; -import { formatFileSize } from '@app/utils/fileUtils'; -import ToolChain from '@app/components/shared/ToolChain'; -import HoverActionMenu, { HoverAction } from '@app/components/shared/HoverActionMenu'; -import { downloadFile } from '@app/services/downloadService'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import UploadToServerModal from '@app/components/shared/UploadToServerModal'; -import ShareFileModal from '@app/components/shared/ShareFileModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { truncateCenter } from '@app/utils/textUtils'; - +import React, { useState, useCallback, useRef, useMemo } from "react"; +import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack, Loader } from "@mantine/core"; +import { useIsMobile } from "@app/hooks/useIsMobile"; +import { alert } from "@app/components/toast"; +import { useTranslation } from "react-i18next"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; +import CloseIcon from "@mui/icons-material/Close"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import UnarchiveIcon from "@mui/icons-material/Unarchive"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import LinkIcon from "@mui/icons-material/Link"; +import PushPinIcon from "@mui/icons-material/PushPin"; +import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; +import LockOpenIcon from "@mui/icons-material/LockOpen"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { StirlingFileStub } from "@app/types/fileContext"; +import { zipFileService } from "@app/services/zipFileService"; +import styles from "@app/components/fileEditor/FileEditor.module.css"; +import { useFileContext } from "@app/contexts/FileContext"; +import { useFileState } from "@app/contexts/file/fileHooks"; +import { FileId } from "@app/types/file"; +import { formatFileSize } from "@app/utils/fileUtils"; +import ToolChain from "@app/components/shared/ToolChain"; +import HoverActionMenu, { HoverAction } from "@app/components/shared/HoverActionMenu"; +import { downloadFile } from "@app/services/downloadService"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import UploadToServerModal from "@app/components/shared/UploadToServerModal"; +import ShareFileModal from "@app/components/shared/ShareFileModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileEditorThumbnailProps { file: StirlingFileStub; @@ -69,14 +67,7 @@ const FileEditorThumbnail = ({ const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); const DownloadOutlinedIcon = icons.download; - const { - pinFile, - unpinFile, - isFilePinned, - activeFiles, - actions: fileActions, - openEncryptedUnlockPrompt, - } = useFileContext(); + const { pinFile, unpinFile, isFilePinned, activeFiles, actions: fileActions, openEncryptedUnlockPrompt } = useFileContext(); const { state, selectors } = useFileState(); const hasError = state.ui.errorFileIds.includes(file.id); @@ -93,7 +84,7 @@ const FileEditorThumbnail = ({ // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { - return activeFiles.find(f => f.fileId === file.id); + return activeFiles.find((f) => f.fileId === file.id); }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; @@ -114,17 +105,17 @@ const FileEditorThumbnail = ({ }, [file.size]); const extUpper = useMemo(() => { - const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ''); - return (m?.[1] || '').toUpperCase(); + const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ""); + return (m?.[1] || "").toUpperCase(); }, [file.name]); const extLower = useMemo(() => { - const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ''); - return (m?.[1] || '').toLowerCase(); + const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ""); + return (m?.[1] || "").toLowerCase(); }, [file.name]); - const isCBZ = extLower === 'cbz'; - const isCBR = extLower === 'cbr'; + const isCBZ = extLower === "cbz"; + const isCBR = extLower === "cbr"; const uploadEnabled = config?.storageEnabled === true; const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true; const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true; @@ -137,71 +128,68 @@ const FileEditorThumbnail = ({ const canUpload = uploadEnabled && isOwnedOrLocal && file.isLeaf && (!isUploaded || !isUpToDate); const canShare = shareLinksEnabled && isOwnedOrLocal && file.isLeaf; - const pageLabel = useMemo( - () => - pageCount > 0 - ? `${pageCount} ${pageCount === 1 ? 'Page' : 'Pages'}` - : '', - [pageCount] - ); + const pageLabel = useMemo(() => (pageCount > 0 ? `${pageCount} ${pageCount === 1 ? "Page" : "Pages"}` : ""), [pageCount]); const dateLabel = useMemo(() => { const d = new Date(file.lastModified); - if (Number.isNaN(d.getTime())) return ''; + if (Number.isNaN(d.getTime())) return ""; return new Intl.DateTimeFormat(undefined, { - month: 'short', - day: '2-digit', - year: 'numeric', + month: "short", + day: "2-digit", + year: "numeric", }).format(d); }, [file.lastModified]); // ---- Drag & drop wiring ---- - const fileElementRef = useCallback((element: HTMLDivElement | null) => { - if (!element) return; + const fileElementRef = useCallback( + (element: HTMLDivElement | null) => { + if (!element) return; - dragElementRef.current = element; + dragElementRef.current = element; - const dragCleanup = draggable({ - element, - getInitialData: () => ({ - type: 'file', - fileId: file.id, - fileName: file.name, - selectedFiles: [file.id] // Always drag only this file, ignore selection state - }), - onDragStart: () => { - setIsDragging(true); - }, - onDrop: () => { - setIsDragging(false); - } - }); + const dragCleanup = draggable({ + element, + getInitialData: () => ({ + type: "file", + fileId: file.id, + fileName: file.name, + selectedFiles: [file.id], // Always drag only this file, ignore selection state + }), + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + }); - const dropCleanup = dropTargetForElements({ - element, - getData: () => ({ - type: 'file', - fileId: file.id - }), - canDrop: ({ source }) => { - const sourceData = source.data; - return sourceData.type === 'file' && sourceData.fileId !== file.id; - }, - onDrop: ({ source }) => { - const sourceData = source.data; - if (sourceData.type === 'file' && onReorderFiles) { - const sourceFileId = sourceData.fileId as FileId; - const selectedFileIds = sourceData.selectedFiles as FileId[]; - onReorderFiles(sourceFileId, file.id, selectedFileIds); - } - } - }); + const dropCleanup = dropTargetForElements({ + element, + getData: () => ({ + type: "file", + fileId: file.id, + }), + canDrop: ({ source }) => { + const sourceData = source.data; + return sourceData.type === "file" && sourceData.fileId !== file.id; + }, + onDrop: ({ source }) => { + const sourceData = source.data; + if (sourceData.type === "file" && onReorderFiles) { + const sourceFileId = sourceData.fileId as FileId; + const selectedFileIds = sourceData.selectedFiles as FileId[]; + onReorderFiles(sourceFileId, file.id, selectedFileIds); + } + }, + }); - return () => { - dragCleanup(); - dropCleanup(); - }; - }, [file.id, file.name, selectedFiles, onReorderFiles]); + return () => { + dragCleanup(); + dropCleanup(); + }; + }, + [file.id, file.name, selectedFiles, onReorderFiles], + ); // Handle close with confirmation const handleCloseWithConfirmation = useCallback(() => { @@ -210,7 +198,7 @@ const FileEditorThumbnail = ({ const handleConfirmClose = useCallback(() => { onCloseFile(file.id); - alert({ alertType: 'neutral', title: `Closed ${file.name}`, expandable: false, durationMs: 3500 }); + alert({ alertType: "neutral", title: `Closed ${file.name}`, expandable: false, durationMs: 3500 }); setShowCloseModal(false); }, [file.id, file.name, onCloseFile]); @@ -221,12 +209,12 @@ const FileEditorThumbnail = ({ const result = await downloadFile({ data: fileToSave, filename: file.name, - localPath: file.localFilePath + localPath: file.localFilePath, }); if (!result.cancelled && result.savedPath) { fileActions.updateStirlingFileStub(file.id, { localFilePath: file.localFilePath ?? result.savedPath, - isDirty: false + isDirty: false, }); } else if (result.cancelled) { setShowCloseModal(false); @@ -234,14 +222,14 @@ const FileEditorThumbnail = ({ } } catch (error) { console.error(`Failed to save ${file.name}:`, error); - alert({ alertType: 'error', title: 'Save failed', body: `Could not save ${file.name}`, expandable: true }); + alert({ alertType: "error", title: "Save failed", body: `Could not save ${file.name}`, expandable: true }); setShowCloseModal(false); return; } } // Then close onCloseFile(file.id); - alert({ alertType: 'success', title: `Saved and closed ${file.name}`, expandable: false, durationMs: 3500 }); + alert({ alertType: "success", title: `Saved and closed ${file.name}`, expandable: false, durationMs: 3500 }); setShowCloseModal(false); }, [file.id, file.name, file.localFilePath, onCloseFile, selectors, fileActions]); @@ -250,95 +238,110 @@ const FileEditorThumbnail = ({ }, []); // Build hover menu actions - const hoverActions = useMemo(() => [ - { - id: 'view', - icon: , - label: t('openInViewer', 'Open in Viewer'), - onClick: (e) => { - e.stopPropagation(); - onViewFile(file.id); + const hoverActions = useMemo( + () => [ + { + id: "view", + icon: , + label: t("openInViewer", "Open in Viewer"), + onClick: (e) => { + e.stopPropagation(); + onViewFile(file.id); + }, }, - }, - { - id: 'download', - icon: , - label: terminology.download, - onClick: (e) => { - e.stopPropagation(); - onDownloadFile(file.id); + { + id: "download", + icon: , + label: terminology.download, + onClick: (e) => { + e.stopPropagation(); + onDownloadFile(file.id); + }, }, - }, - ...(canUpload || canShare - ? [ - ...(canUpload ? [{ - id: 'upload', - icon: , - label: isUploaded - ? t('fileManager.updateOnServer', 'Update on Server') - : t('fileManager.uploadToServer', 'Upload to Server'), - onClick: (e: React.MouseEvent) => { - e.stopPropagation(); - setShowUploadModal(true); - }, - }] : []), - ...(canShare ? [{ - id: 'share', - icon: , - label: t('fileManager.share', 'Share'), - onClick: (e: React.MouseEvent) => { - e.stopPropagation(); - setShowShareModal(true); - }, - }] : []), - ] - : []), - { - id: 'unzip', - icon: , - label: t('fileManager.unzip', 'Unzip'), - onClick: (e) => { - e.stopPropagation(); - if (onUnzipFile) { - onUnzipFile(file.id); - alert({ alertType: 'success', title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 }); - } + ...(canUpload || canShare + ? [ + ...(canUpload + ? [ + { + id: "upload", + icon: , + label: isUploaded + ? t("fileManager.updateOnServer", "Update on Server") + : t("fileManager.uploadToServer", "Upload to Server"), + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + setShowUploadModal(true); + }, + }, + ] + : []), + ...(canShare + ? [ + { + id: "share", + icon: , + label: t("fileManager.share", "Share"), + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + setShowShareModal(true); + }, + }, + ] + : []), + ] + : []), + { + id: "unzip", + icon: , + label: t("fileManager.unzip", "Unzip"), + onClick: (e) => { + e.stopPropagation(); + if (onUnzipFile) { + onUnzipFile(file.id); + alert({ alertType: "success", title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 }); + } + }, + hidden: !isZipFile || !onUnzipFile || isCBZ || isCBR, }, - hidden: !isZipFile || !onUnzipFile || isCBZ || isCBR, - }, - { - id: 'close', - icon: , - label: t('close', 'Close'), - onClick: (e) => { - e.stopPropagation(); - handleCloseWithConfirmation(); + { + id: "close", + icon: , + label: t("close", "Close"), + onClick: (e) => { + e.stopPropagation(); + handleCloseWithConfirmation(); + }, + color: "red", }, - color: 'red', - } - ], [ - t, - file.id, - file.name, - isZipFile, - isCBZ, - isCBR, - terminology, - onViewFile, - onDownloadFile, - onUnzipFile, - handleCloseWithConfirmation, - canUpload, - canShare, - isUploaded - ]); + ], + [ + t, + file.id, + file.name, + isZipFile, + isCBZ, + isCBR, + terminology, + onViewFile, + onDownloadFile, + onUnzipFile, + handleCloseWithConfirmation, + canUpload, + canShare, + isUploaded, + ], + ); // ---- Card interactions ---- const handleCardClick = () => { if (!isSupported) return; // Clear error state if file has an error (click to clear error) if (hasError) { - try { fileActions.clearFileError(file.id); } catch (_e) { void _e; } + try { + fileActions.clearFileError(file.id); + } catch (_e) { + void _e; + } } if (isSharedFile && !sharedEditNoticeShownRef.current) { sharedEditNoticeShownRef.current = true; @@ -359,7 +362,6 @@ const FileEditorThumbnail = ({ return isSelected ? styles.headerSelected : styles.headerResting; }; - return (
{/* Header bar */} -
+
{/* Logo/checkbox area */}
{hasError ? (
- {t('error._value', 'Error')} + {t("error._value", "Error")}
) : isSupported ? ( ) : (
- - {t('unsupported', 'Unsupported')} - + {t("unsupported", "Unsupported")}
)}
@@ -412,9 +409,9 @@ const FileEditorThumbnail = ({ {/* Action buttons group */}
{isEncrypted && ( - + { @@ -427,9 +424,17 @@ const FileEditorThumbnail = ({ )} {/* Pin/Unpin icon */} - + + style={{ + padding: "0.5rem", + textAlign: "center", + background: "var(--file-card-bg)", + marginTop: "0.5rem", + marginBottom: "0.5rem", + }} + > {truncateCenter(file.name, 40)} - + {/* e.g., v2 - Jan 29, 2025 - PDF file - 3 Pages */} {`v${file.versionNumber} - `} {dateLabel} - {extUpper ? ` - ${extUpper} file` : ''} - {pageLabel ? ` - ${pageLabel}` : ''} + {extUpper ? ` - ${extUpper} file` : ""} + {pageLabel ? ` - ${pageLabel}` : ""}
{/* Preview area */}
{file.thumbnailUrl ? ( @@ -495,27 +495,29 @@ const FileEditorThumbnail = ({ decoding="async" onError={(e) => { const img = e.currentTarget; - img.style.display = 'none'; - img.parentElement?.setAttribute('data-thumb-missing', 'true'); + img.style.display = "none"; + img.parentElement?.setAttribute("data-thumb-missing", "true"); }} style={{ - maxWidth: '80%', - maxHeight: '80%', - objectFit: 'contain', - borderRadius: 0, - background: '#ffffff', - border: '1px solid var(--border-default)', - display: 'block', - marginLeft: 'auto', - marginRight: 'auto', - alignSelf: 'start' - }} - /> + maxWidth: "80%", + maxHeight: "80%", + objectFit: "contain", + borderRadius: 0, + background: "#ffffff", + border: "1px solid var(--border-default)", + display: "block", + marginLeft: "auto", + marginRight: "auto", + alignSelf: "start", + }} + /> - ) : file.type?.startsWith('application/pdf') ? ( - + ) : file.type?.startsWith("application/pdf") ? ( + - Loading thumbnail... + + Loading thumbnail... + ) : null}
@@ -527,74 +529,72 @@ const FileEditorThumbnail = ({ {/* Tool chain display at bottom */} {file.toolHistory && ( -
+
)}
{/* Hover Menu */} - + {/* Close Confirmation Modal */} {file.isDirty && file.localFilePath ? ( <> - {t('confirmCloseUnsaved', 'This file has unsaved changes.')} + {t("confirmCloseUnsaved", "This file has unsaved changes.")} {file.name} ) : ( <> - {t('confirmCloseMessage', 'Are you sure you want to close this file?')} + {t("confirmCloseMessage", "Are you sure you want to close this file?")} {file.name} @@ -604,39 +604,27 @@ const FileEditorThumbnail = ({ setShowSharedEditNotice(false)} - title={t('fileManager.sharedEditNoticeTitle', 'Read-only server copy')} + title={t("fileManager.sharedEditNoticeTitle", "Read-only server copy")} centered size="auto" > {t( - 'fileManager.sharedEditNoticeBody', - 'You do not have edit rights to the server version of this file. Any edits you make will be saved as a local copy.' + "fileManager.sharedEditNoticeBody", + "You do not have edit rights to the server version of this file. Any edits you make will be saved as a local copy.", )} - {canUpload && ( - setShowUploadModal(false)} - file={file} - /> - )} - {canShare && ( - setShowShareModal(false)} - file={file} - /> - )} + {canUpload && setShowUploadModal(false)} file={file} />} + {canShare && setShowShareModal(false)} file={file} />}
); }; diff --git a/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx b/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx index 122023af6f..95de665902 100644 --- a/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx +++ b/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx @@ -1,7 +1,7 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRightRailButtons, RightRailButtonWithAction } from '@app/hooks/useRightRailButtons'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useRightRailButtons, RightRailButtonWithAction } from "@app/hooks/useRightRailButtons"; +import LocalIcon from "@app/components/shared/LocalIcon"; interface FileEditorRightRailButtonsParams { totalItems: number; @@ -20,41 +20,44 @@ export function useFileEditorRightRailButtons({ }: FileEditorRightRailButtonsParams) { const { t, i18n } = useTranslation(); - const buttons = useMemo(() => [ - { - id: 'file-select-all', - icon: , - tooltip: t('rightRail.selectAll', 'Select All'), - ariaLabel: typeof t === 'function' ? t('rightRail.selectAll', 'Select All') : 'Select All', - section: 'top' as const, - order: 10, - disabled: totalItems === 0 || selectedCount === totalItems, - visible: totalItems > 0, - onClick: onSelectAll, - }, - { - id: 'file-deselect-all', - icon: , - tooltip: t('rightRail.deselectAll', 'Deselect All'), - ariaLabel: typeof t === 'function' ? t('rightRail.deselectAll', 'Deselect All') : 'Deselect All', - section: 'top' as const, - order: 20, - disabled: selectedCount === 0, - visible: totalItems > 0, - onClick: onDeselectAll, - }, - { - id: 'file-close-selected', - icon: , - tooltip: t('rightRail.closeSelected', 'Close Selected Files'), - ariaLabel: typeof t === 'function' ? t('rightRail.closeSelected', 'Close Selected Files') : 'Close Selected Files', - section: 'top' as const, - order: 30, - disabled: selectedCount === 0, - visible: totalItems > 0, - onClick: onCloseSelected, - }, - ], [t, i18n.language, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected]); + const buttons = useMemo( + () => [ + { + id: "file-select-all", + icon: , + tooltip: t("rightRail.selectAll", "Select All"), + ariaLabel: typeof t === "function" ? t("rightRail.selectAll", "Select All") : "Select All", + section: "top" as const, + order: 10, + disabled: totalItems === 0 || selectedCount === totalItems, + visible: totalItems > 0, + onClick: onSelectAll, + }, + { + id: "file-deselect-all", + icon: , + tooltip: t("rightRail.deselectAll", "Deselect All"), + ariaLabel: typeof t === "function" ? t("rightRail.deselectAll", "Deselect All") : "Deselect All", + section: "top" as const, + order: 20, + disabled: selectedCount === 0, + visible: totalItems > 0, + onClick: onDeselectAll, + }, + { + id: "file-close-selected", + icon: , + tooltip: t("rightRail.closeSelected", "Close Selected Files"), + ariaLabel: typeof t === "function" ? t("rightRail.closeSelected", "Close Selected Files") : "Close Selected Files", + section: "top" as const, + order: 30, + disabled: selectedCount === 0, + visible: totalItems > 0, + onClick: onCloseSelected, + }, + ], + [t, i18n.language, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected], + ); useRightRailButtons(buttons); } diff --git a/frontend/src/core/components/fileManager/CompactFileDetails.tsx b/frontend/src/core/components/fileManager/CompactFileDetails.tsx index 5156dccff9..9e60b8d8e6 100644 --- a/frontend/src/core/components/fileManager/CompactFileDetails.tsx +++ b/frontend/src/core/components/fileManager/CompactFileDetails.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { Stack, Box, Text, Button, ActionIcon, Center } from '@mantine/core'; -import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import { useTranslation } from 'react-i18next'; -import { getFileSize } from '@app/utils/fileUtils'; -import { StirlingFileStub } from '@app/types/fileContext'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; +import React from "react"; +import { Stack, Box, Text, Button, ActionIcon, Center } from "@mantine/core"; +import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { useTranslation } from "react-i18next"; +import { getFileSize } from "@app/utils/fileUtils"; +import { StirlingFileStub } from "@app/types/fileContext"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; interface CompactFileDetailsProps { currentFile: StirlingFileStub | null; @@ -29,47 +29,56 @@ const CompactFileDetails: React.FC = ({ isAnimating, onPrevious, onNext, - onOpenFiles + onOpenFiles, }) => { const { t } = useTranslation(); const hasSelection = selectedFiles.length > 0; const hasMultipleFiles = numberOfFiles > 1; const showOwner = Boolean( - currentFile && - (currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink) + currentFile && (currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink), ); - const ownerLabel = currentFile - ? currentFile.remoteOwnerUsername || t('fileManager.ownerUnknown', 'Unknown') - : ''; + const ownerLabel = currentFile ? currentFile.remoteOwnerUsername || t("fileManager.ownerUnknown", "Unknown") : ""; return ( - + {/* Compact mobile layout */} - + {/* Small preview */} - + {currentFile && thumbnail ? ( {currentFile.name} ) : currentFile ? ( -
- +
+
) : null} @@ -77,10 +86,10 @@ const CompactFileDetails: React.FC = ({ {/* File info */} - {currentFile ? currentFile.name : 'No file selected'} + {currentFile ? currentFile.name : "No file selected"} - {currentFile ? getFileSize(currentFile) : ''} + {currentFile ? getFileSize(currentFile) : ""} {selectedFiles.length > 1 && ` • ${selectedFiles.length} files`} {currentFile && ` • v${currentFile.versionNumber || 1}`} @@ -92,33 +101,23 @@ const CompactFileDetails: React.FC = ({ {/* Compact tool chain for mobile */} {currentFile?.toolHistory && currentFile.toolHistory.length > 0 && ( - {currentFile.toolHistory.map((tool) => t(`home.${tool.toolId}.title`, tool.toolId)).join(' → ')} + {currentFile.toolHistory.map((tool) => t(`home.${tool.toolId}.title`, tool.toolId)).join(" → ")} )} {currentFile && showOwner && ( - {t('fileManager.owner', 'Owner')}: {ownerLabel} + {t("fileManager.owner", "Owner")}: {ownerLabel} )} {/* Navigation arrows for multiple files */} {hasMultipleFiles && ( - - + + - + @@ -132,14 +131,13 @@ const CompactFileDetails: React.FC = ({ disabled={!hasSelection} fullWidth style={{ - backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)', - color: 'white' + backgroundColor: hasSelection ? "var(--btn-open-file)" : "var(--mantine-color-gray-4)", + color: "white", }} > {selectedFiles.length > 1 - ? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`) - : t('fileManager.openFile', 'Open File') - } + ? t("fileManager.openFiles", `Open ${selectedFiles.length} Files`) + : t("fileManager.openFile", "Open File")} ); diff --git a/frontend/src/core/components/fileManager/DesktopLayout.tsx b/frontend/src/core/components/fileManager/DesktopLayout.tsx index 9926592c84..057b70c021 100644 --- a/frontend/src/core/components/fileManager/DesktopLayout.tsx +++ b/frontend/src/core/components/fileManager/DesktopLayout.tsx @@ -1,79 +1,85 @@ -import React from 'react'; -import { Grid } from '@mantine/core'; -import FileSourceButtons from '@app/components/fileManager/FileSourceButtons'; -import FileDetails from '@app/components/fileManager/FileDetails'; -import SearchInput from '@app/components/fileManager/SearchInput'; -import FileListArea from '@app/components/fileManager/FileListArea'; -import FileActions from '@app/components/fileManager/FileActions'; -import HiddenFileInput from '@app/components/fileManager/HiddenFileInput'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { Grid } from "@mantine/core"; +import FileSourceButtons from "@app/components/fileManager/FileSourceButtons"; +import FileDetails from "@app/components/fileManager/FileDetails"; +import SearchInput from "@app/components/fileManager/SearchInput"; +import FileListArea from "@app/components/fileManager/FileListArea"; +import FileActions from "@app/components/fileManager/FileActions"; +import HiddenFileInput from "@app/components/fileManager/HiddenFileInput"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; const DesktopLayout: React.FC = () => { - const { - activeSource, - recentFiles, - modalHeight, - } = useFileManagerContext(); + const { activeSource, recentFiles, modalHeight } = useFileManagerContext(); return ( - + {/* Column 1: File Sources */} - + {/* Column 2: File List */} - -
- {activeSource === 'recent' && ( + +
+ {activeSource === "recent" && ( <> -
+
-
+
)} -
+
0 - ? `calc(${modalHeight} - 7rem)` - : '100%'} + scrollAreaHeight={activeSource === "recent" && recentFiles.length > 0 ? `calc(${modalHeight} - 7rem)` : "100%"} scrollAreaStyle={{ - height: activeSource === 'recent' && recentFiles.length > 0 - ? `calc(${modalHeight} - 7rem)` - : '100%', - backgroundColor: 'transparent', - border: 'none', - borderRadius: 0 + height: activeSource === "recent" && recentFiles.length > 0 ? `calc(${modalHeight} - 7rem)` : "100%", + backgroundColor: "transparent", + border: "none", + borderRadius: 0, }} />
@@ -81,14 +87,18 @@ const DesktopLayout: React.FC = () => { {/* Column 3: File Details */} - -
+ +
diff --git a/frontend/src/core/components/fileManager/DragOverlay.tsx b/frontend/src/core/components/fileManager/DragOverlay.tsx index 976bb940e9..de3409d90d 100644 --- a/frontend/src/core/components/fileManager/DragOverlay.tsx +++ b/frontend/src/core/components/fileManager/DragOverlay.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Stack, Text, useMantineTheme, alpha } from '@mantine/core'; -import UploadFileIcon from '@mui/icons-material/UploadFile'; -import { useTranslation } from 'react-i18next'; +import React from "react"; +import { Stack, Text, useMantineTheme, alpha } from "@mantine/core"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { useTranslation } from "react-i18next"; interface DragOverlayProps { isVisible: boolean; @@ -16,29 +16,29 @@ const DragOverlay: React.FC = ({ isVisible }) => { return (
- + - {t('fileManager.dropFilesHere', 'Drop files here to upload')} + {t("fileManager.dropFilesHere", "Drop files here to upload")}
); }; -export default DragOverlay; \ No newline at end of file +export default DragOverlay; diff --git a/frontend/src/core/components/fileManager/EmptyFilesState.tsx b/frontend/src/core/components/fileManager/EmptyFilesState.tsx index 24e79fb120..b774f06042 100644 --- a/frontend/src/core/components/fileManager/EmptyFilesState.tsx +++ b/frontend/src/core/components/fileManager/EmptyFilesState.tsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react'; -import { Button, Group, Text, Stack, useMantineColorScheme } from '@mantine/core'; -import HistoryIcon from '@mui/icons-material/History'; -import { useTranslation } from 'react-i18next'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; +import React, { useState } from "react"; +import { Button, Group, Text, Stack, useMantineColorScheme } from "@mantine/core"; +import HistoryIcon from "@mui/icons-material/History"; +import { useTranslation } from "react-i18next"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; const EmptyFilesState: React.FC = () => { const { t } = useTranslation(); @@ -24,92 +24,90 @@ const EmptyFilesState: React.FC = () => { return (
{/* Container */}
{/* No Recent Files Message */} - + - {t('fileManager.noRecentFiles', 'No recent files')} + {t("fileManager.noRecentFiles", "No recent files")} {/* Stirling PDF Logo */} Stirling PDF {/* Upload Button */}
setIsUploadHover(false)} >
{/* Instruction Text */} - + {terminology.dropFilesHere}
diff --git a/frontend/src/core/components/fileManager/FileActions.tsx b/frontend/src/core/components/fileManager/FileActions.tsx index 2e69c32d35..e39113fe6d 100644 --- a/frontend/src/core/components/fileManager/FileActions.tsx +++ b/frontend/src/core/components/fileManager/FileActions.tsx @@ -30,9 +30,8 @@ const FileActions: React.FC = () => { onDownloadSelected, refreshRecentFiles, storageFilter, - onStorageFilterChange - } = - useFileManagerContext(); + onStorageFilterChange, + } = useFileManagerContext(); const uploadEnabled = config?.storageEnabled === true; const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true; const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true; @@ -42,11 +41,11 @@ const FileActions: React.FC = () => { { value: "all", label: t("fileManager.filterAll", "All") }, { value: "local", label: t("fileManager.filterLocal", "Local") }, { value: "sharedWithMe", label: t("fileManager.filterSharedWithMe", "Shared with me") }, - { value: "sharedByMe", label: t("fileManager.filterSharedByMe", "Shared by me") } + { value: "sharedByMe", label: t("fileManager.filterSharedByMe", "Shared by me") }, ] : [ { value: "all", label: t("fileManager.filterAll", "All") }, - { value: "local", label: t("fileManager.filterLocal", "Local") } + { value: "local", label: t("fileManager.filterLocal", "Local") }, ]; useEffect(() => { if (!sharingEnabled && (storageFilter === "sharedWithMe" || storageFilter === "sharedByMe")) { @@ -56,10 +55,8 @@ const FileActions: React.FC = () => { const hasSelection = selectedFileIds.length > 0; const hasOnlyOwnedSelection = selectedFiles.every((file) => file.remoteOwnedByCurrentUser !== false); const hasDownloadAccess = selectedFiles.every((file) => { - const role = (file.remoteOwnedByCurrentUser !== false - ? 'editor' - : (file.remoteAccessRole ?? 'viewer')).toLowerCase(); - return role === 'editor' || role === 'commenter' || role === 'viewer'; + const role = (file.remoteOwnedByCurrentUser !== false ? "editor" : (file.remoteAccessRole ?? "viewer")).toLowerCase(); + return role === "editor" || role === "commenter" || role === "viewer"; }); const canBulkUpload = uploadEnabled && hasSelection && hasOnlyOwnedSelection; const canBulkShare = shareLinksEnabled && hasSelection && hasOnlyOwnedSelection; @@ -80,7 +77,6 @@ const FileActions: React.FC = () => { } }; - // Only show actions if there are files if (recentFiles.length === 0) { return null; @@ -120,9 +116,7 @@ const FileActions: React.FC = () => { - onStorageFilterChange(value as "all" | "local" | "sharedWithMe" | "sharedByMe") - } + onChange={(value) => onStorageFilterChange(value as "all" | "local" | "sharedWithMe" | "sharedByMe")} data={storageFilterOptions} /> )} diff --git a/frontend/src/core/components/fileManager/FileDetails.tsx b/frontend/src/core/components/fileManager/FileDetails.tsx index c2afc47769..969b98e699 100644 --- a/frontend/src/core/components/fileManager/FileDetails.tsx +++ b/frontend/src/core/components/fileManager/FileDetails.tsx @@ -1,19 +1,17 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, Button, Box } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useIndexedDBThumbnail } from '@app/hooks/useIndexedDBThumbnail'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import FilePreview from '@app/components/shared/FilePreview'; -import FileInfoCard from '@app/components/fileManager/FileInfoCard'; -import CompactFileDetails from '@app/components/fileManager/CompactFileDetails'; +import React, { useEffect, useState } from "react"; +import { Stack, Button, Box } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useIndexedDBThumbnail } from "@app/hooks/useIndexedDBThumbnail"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import FilePreview from "@app/components/shared/FilePreview"; +import FileInfoCard from "@app/components/fileManager/FileInfoCard"; +import CompactFileDetails from "@app/components/fileManager/CompactFileDetails"; interface FileDetailsProps { compact?: boolean; } -const FileDetails: React.FC = ({ - compact = false -}) => { +const FileDetails: React.FC = ({ compact = false }) => { const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext(); const { t } = useTranslation(); const [currentFileIndex, setCurrentFileIndex] = useState(0); @@ -35,7 +33,7 @@ const FileDetails: React.FC = ({ if (isAnimating) return; setIsAnimating(true); setTimeout(() => { - setCurrentFileIndex(prev => prev > 0 ? prev - 1 : selectedFiles.length - 1); + setCurrentFileIndex((prev) => (prev > 0 ? prev - 1 : selectedFiles.length - 1)); setIsAnimating(false); }, 150); }; @@ -44,7 +42,7 @@ const FileDetails: React.FC = ({ if (isAnimating) return; setIsAnimating(true); setTimeout(() => { - setCurrentFileIndex(prev => prev < selectedFiles.length - 1 ? prev + 1 : 0); + setCurrentFileIndex((prev) => (prev < selectedFiles.length - 1 ? prev + 1 : 0)); setIsAnimating(false); }, 150); }; @@ -75,7 +73,7 @@ const FileDetails: React.FC = ({ return ( {/* Section 1: Thumbnail Preview */} - + = ({ {/* Section 2: File Details */} - + ); diff --git a/frontend/src/core/components/fileManager/FileHistoryGroup.tsx b/frontend/src/core/components/fileManager/FileHistoryGroup.tsx index 75d8f0e50d..58edbc46cc 100644 --- a/frontend/src/core/components/fileManager/FileHistoryGroup.tsx +++ b/frontend/src/core/components/fileManager/FileHistoryGroup.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { Box, Text, Collapse, Group } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { StirlingFileStub } from '@app/types/fileContext'; -import FileListItem from '@app/components/fileManager/FileListItem'; +import React from "react"; +import { Box, Text, Collapse, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { StirlingFileStub } from "@app/types/fileContext"; +import FileListItem from "@app/components/fileManager/FileListItem"; interface FileHistoryGroupProps { leafFile: StirlingFileStub; @@ -27,7 +27,7 @@ const FileHistoryGroup: React.FC = ({ // Sort history files by version number (oldest first, excluding the current leaf file) const sortedHistory = historyFiles - .filter(file => file.id !== leafFile.id) // Exclude the leaf file itself + .filter((file) => file.id !== leafFile.id) // Exclude the leaf file itself .sort((a, b) => (b.versionNumber || 1) - (a.versionNumber || 1)); if (!isExpanded || sortedHistory.length === 0) { @@ -39,7 +39,7 @@ const FileHistoryGroup: React.FC = ({ - {t('fileManager.fileHistory', 'File History')} ({sortedHistory.length}) + {t("fileManager.fileHistory", "File History")} ({sortedHistory.length}) diff --git a/frontend/src/core/components/fileManager/FileInfoCard.tsx b/frontend/src/core/components/fileManager/FileInfoCard.tsx index 182fcded9d..1c47c71bd2 100644 --- a/frontend/src/core/components/fileManager/FileInfoCard.tsx +++ b/frontend/src/core/components/fileManager/FileInfoCard.tsx @@ -1,23 +1,20 @@ -import React, { useMemo, useState } from 'react'; -import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea, Button } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { detectFileExtension, getFileSize } from '@app/utils/fileUtils'; -import { StirlingFileStub } from '@app/types/fileContext'; -import ToolChain from '@app/components/shared/ToolChain'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import ShareManagementModal from '@app/components/shared/ShareManagementModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; +import React, { useMemo, useState } from "react"; +import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea, Button } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { detectFileExtension, getFileSize } from "@app/utils/fileUtils"; +import { StirlingFileStub } from "@app/types/fileContext"; +import ToolChain from "@app/components/shared/ToolChain"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import ShareManagementModal from "@app/components/shared/ShareManagementModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; interface FileInfoCardProps { currentFile: StirlingFileStub | null; modalHeight: string; } -const FileInfoCard: React.FC = ({ - currentFile, - modalHeight -}) => { +const FileInfoCard: React.FC = ({ currentFile, modalHeight }) => { const { t } = useTranslation(); const { config } = useAppConfig(); const { onMakeCopy } = useFileManagerContext(); @@ -43,38 +40,53 @@ const FileInfoCard: React.FC = ({ const uploadEnabled = config?.storageEnabled === true; const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true; const ownerLabel = useMemo(() => { - if (!currentFile) return ''; + if (!currentFile) return ""; if (currentFile.remoteOwnerUsername) { return currentFile.remoteOwnerUsername; } - return t('fileManager.ownerUnknown', 'Unknown'); + return t("fileManager.ownerUnknown", "Unknown"); }, [currentFile, t]); const lastSyncedLabel = useMemo(() => { - if (!currentFile?.remoteStorageUpdatedAt) return ''; + if (!currentFile?.remoteStorageUpdatedAt) return ""; return new Date(currentFile.remoteStorageUpdatedAt).toLocaleString(); }, [currentFile?.remoteStorageUpdatedAt]); return ( - - + + - {t('fileManager.details', 'File Details')} + {t("fileManager.details", "File Details")} - {t('fileManager.fileName', 'Name')} + {t("fileManager.fileName", "Name")} - - {currentFile ? currentFile.name : ''} + + {currentFile ? currentFile.name : ""} - {t('fileManager.fileFormat', 'Format')} + + {t("fileManager.fileFormat", "Format")} + {currentFile ? ( {detectFileExtension(currentFile.name).toUpperCase()} @@ -86,38 +98,48 @@ const FileInfoCard: React.FC = ({ - {t('fileManager.fileSize', 'Size')} + + {t("fileManager.fileSize", "Size")} + - {currentFile ? getFileSize(currentFile) : ''} + {currentFile ? getFileSize(currentFile) : ""} - {t('fileManager.lastModified', 'Last modified')} + + {t("fileManager.lastModified", "Last modified")} + - {currentFile ? new Date(currentFile.lastModified).toLocaleDateString() : ''} + {currentFile ? new Date(currentFile.lastModified).toLocaleDateString() : ""} - {t('fileManager.fileVersion', 'Version')} - {currentFile && - - v{currentFile ? (currentFile.versionNumber || 1) : ''} - } - + + {t("fileManager.fileVersion", "Version")} + + {currentFile && ( + + v{currentFile ? currentFile.versionNumber || 1 : ""} + + )} {sharingEnabled && isSharedWithYou && ( <> - {t('fileManager.owner', 'Owner')} + + {t("fileManager.owner", "Owner")} + - {ownerLabel} + + {ownerLabel} + - {t('fileManager.sharedWithYou', 'Shared with you')} + {t("fileManager.sharedWithYou", "Shared with you")} @@ -129,12 +151,10 @@ const FileInfoCard: React.FC = ({ <> - {t('fileManager.toolChain', 'Tools Applied')} - + + {t("fileManager.toolChain", "Tools Applied")} + + )} @@ -142,13 +162,8 @@ const FileInfoCard: React.FC = ({ {currentFile && isSharedWithYou && ( <> - )} @@ -157,39 +172,42 @@ const FileInfoCard: React.FC = ({ <> - {t('fileManager.cloudFile', 'Cloud file')} + + {t("fileManager.cloudFile", "Cloud file")} + {uploadEnabled && isOutOfSync ? ( - {t('fileManager.changesNotUploaded', 'Changes not uploaded')} + {t("fileManager.changesNotUploaded", "Changes not uploaded")} ) : uploadEnabled ? ( - {t('fileManager.synced', 'Synced')} + {t("fileManager.synced", "Synced")} ) : null} {lastSyncedLabel && ( - {t('fileManager.lastSynced', 'Last synced')} - {lastSyncedLabel} + + {t("fileManager.lastSynced", "Last synced")} + + + {lastSyncedLabel} + )} {isSharedByYou && sharingEnabled && ( <> - {t('fileManager.sharing', 'Sharing')} + + {t("fileManager.sharing", "Sharing")} + - {t('fileManager.sharedByYou', 'Shared by you')} + {t("fileManager.sharedByYou", "Shared by you")} - )} @@ -199,9 +217,11 @@ const FileInfoCard: React.FC = ({ <> - {t('fileManager.storageState', 'Storage')} + + {t("fileManager.storageState", "Storage")} + - {t('fileManager.localOnly', 'Local only')} + {t("fileManager.localOnly", "Local only")} diff --git a/frontend/src/core/components/fileManager/FileListArea.tsx b/frontend/src/core/components/fileManager/FileListArea.tsx index 1964dad542..b088a55d53 100644 --- a/frontend/src/core/components/fileManager/FileListArea.tsx +++ b/frontend/src/core/components/fileManager/FileListArea.tsx @@ -1,21 +1,18 @@ -import React from 'react'; -import { Center, ScrollArea, Text, Stack } from '@mantine/core'; -import CloudIcon from '@mui/icons-material/Cloud'; -import { useTranslation } from 'react-i18next'; -import FileListItem from '@app/components/fileManager/FileListItem'; -import FileHistoryGroup from '@app/components/fileManager/FileHistoryGroup'; -import EmptyFilesState from '@app/components/fileManager/EmptyFilesState'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { Center, ScrollArea, Text, Stack } from "@mantine/core"; +import CloudIcon from "@mui/icons-material/Cloud"; +import { useTranslation } from "react-i18next"; +import FileListItem from "@app/components/fileManager/FileListItem"; +import FileHistoryGroup from "@app/components/fileManager/FileHistoryGroup"; +import EmptyFilesState from "@app/components/fileManager/EmptyFilesState"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; interface FileListAreaProps { scrollAreaHeight: string; scrollAreaStyle?: React.CSSProperties; } -const FileListArea: React.FC = ({ - scrollAreaHeight, - scrollAreaStyle = {}, -}) => { +const FileListArea: React.FC = ({ scrollAreaHeight, scrollAreaStyle = {} }) => { const { activeSource, recentFiles, @@ -34,12 +31,12 @@ const FileListArea: React.FC = ({ } = useFileManagerContext(); const { t } = useTranslation(); - if (activeSource === 'recent') { + if (activeSource === "recent") { return ( = ({ {recentFiles.length === 0 && !isLoading ? ( ) : recentFiles.length === 0 && isLoading ? ( -
- {t('fileManager.loadingFiles', 'Loading files...')} +
+ + {t("fileManager.loadingFiles", "Loading files...")} +
) : ( filteredFiles.map((file, index) => { @@ -93,10 +92,12 @@ const FileListArea: React.FC = ({ // Google Drive placeholder return ( -
+
- - {t('fileManager.googleDriveNotAvailable', 'Google Drive integration coming soon')} + + + {t("fileManager.googleDriveNotAvailable", "Google Drive integration coming soon")} +
); diff --git a/frontend/src/core/components/fileManager/FileListItem.tsx b/frontend/src/core/components/fileManager/FileListItem.tsx index 73ae8bd2a2..02c3c07f7d 100644 --- a/frontend/src/core/components/fileManager/FileListItem.tsx +++ b/frontend/src/core/components/fileManager/FileListItem.tsx @@ -1,31 +1,31 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge } from '@mantine/core'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import DeleteIcon from '@mui/icons-material/Delete'; -import DownloadIcon from '@mui/icons-material/Download'; -import HistoryIcon from '@mui/icons-material/History'; -import RestoreIcon from '@mui/icons-material/Restore'; -import UnarchiveIcon from '@mui/icons-material/Unarchive'; -import CloseIcon from '@mui/icons-material/Close'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import CloudDoneIcon from '@mui/icons-material/CloudDone'; -import LinkIcon from '@mui/icons-material/Link'; -import { useTranslation } from 'react-i18next'; -import { getFileSize, getFileDate } from '@app/utils/fileUtils'; -import { FileId, StirlingFileStub } from '@app/types/fileContext'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import { zipFileService } from '@app/services/zipFileService'; -import ToolChain from '@app/components/shared/ToolChain'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { useFileManagement } from '@app/contexts/FileContext'; -import UploadToServerModal from '@app/components/shared/UploadToServerModal'; -import ShareFileModal from '@app/components/shared/ShareFileModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import ShareManagementModal from '@app/components/shared/ShareManagementModal'; -import apiClient from '@app/services/apiClient'; -import { absoluteWithBasePath } from '@app/constants/app'; -import { alert } from '@app/components/toast'; +import React, { useCallback, useMemo, useState } from "react"; +import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge } from "@mantine/core"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DownloadIcon from "@mui/icons-material/Download"; +import HistoryIcon from "@mui/icons-material/History"; +import RestoreIcon from "@mui/icons-material/Restore"; +import UnarchiveIcon from "@mui/icons-material/Unarchive"; +import CloseIcon from "@mui/icons-material/Close"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import CloudDoneIcon from "@mui/icons-material/CloudDone"; +import LinkIcon from "@mui/icons-material/Link"; +import { useTranslation } from "react-i18next"; +import { getFileSize, getFileDate } from "@app/utils/fileUtils"; +import { FileId, StirlingFileStub } from "@app/types/fileContext"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import { zipFileService } from "@app/services/zipFileService"; +import ToolChain from "@app/components/shared/ToolChain"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { useFileManagement } from "@app/contexts/FileContext"; +import UploadToServerModal from "@app/components/shared/UploadToServerModal"; +import ShareFileModal from "@app/components/shared/ShareFileModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import ShareManagementModal from "@app/components/shared/ShareManagementModal"; +import apiClient from "@app/services/apiClient"; +import { absoluteWithBasePath } from "@app/constants/app"; +import { alert } from "@app/components/toast"; interface FileListItemProps { file: StirlingFileStub; @@ -51,7 +51,7 @@ const FileListItem: React.FC = ({ onDoubleClick, isHistoryFile = false, isLatestVersion = false, - isActive = false + isActive = false, }) => { const [isHovered, setIsHovered] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -60,22 +60,22 @@ const FileListItem: React.FC = ({ const [showShareManageModal, setShowShareManageModal] = useState(false); const { t } = useTranslation(); const { config } = useAppConfig(); - const {expandedFileIds, onToggleExpansion, onUnzipFile, refreshRecentFiles } = useFileManagerContext(); + const { expandedFileIds, onToggleExpansion, onUnzipFile, refreshRecentFiles } = useFileManagerContext(); const { removeFiles } = useFileManagement(); // Check if this is a ZIP file const isZipFile = zipFileService.isZipFileStub(file); // Check file extension - const extLower = (file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase(); - const isCBZ = extLower === 'cbz'; - const isCBR = extLower === 'cbr'; + const extLower = (file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || "").toLowerCase(); + const isCBZ = extLower === "cbz"; + const isCBR = extLower === "cbr"; // Keep item in hovered state if menu is open const shouldShowHovered = isHovered || isMenuOpen; // Get version information for this file - const leafFileId = (isLatestVersion ? file.id : (file.originalFileId || file.id)) as FileId; + const leafFileId = (isLatestVersion ? file.id : file.originalFileId || file.id) as FileId; const hasVersionHistory = (file.versionNumber || 1) > 1; // Show history for any processed file (v2+) const currentVersion = file.versionNumber || 1; // Display original files as v1 const isExpanded = expandedFileIds.has(leafFileId); @@ -83,32 +83,28 @@ const FileListItem: React.FC = ({ const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true; const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true; const isOwnedOrLocal = file.remoteOwnedByCurrentUser !== false; - const isSharedWithYou = - sharingEnabled && (file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink); + const isSharedWithYou = sharingEnabled && (file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink); const localUpdatedAt = file.createdAt ?? file.lastModified ?? 0; const remoteUpdatedAt = file.remoteStorageUpdatedAt ?? 0; const isUploaded = Boolean(file.remoteStorageId); const isUpToDate = isUploaded && remoteUpdatedAt >= localUpdatedAt; const isOutOfSync = isUploaded && !isUpToDate && isOwnedOrLocal; const isLocalOnly = !file.remoteStorageId && !file.remoteSharedViaLink; - const accessRole = (isOwnedOrLocal ? 'editor' : (file.remoteAccessRole ?? 'viewer')).toLowerCase(); - const hasReadAccess = isOwnedOrLocal || accessRole === 'editor' || accessRole === 'commenter' || accessRole === 'viewer'; + const accessRole = (isOwnedOrLocal ? "editor" : (file.remoteAccessRole ?? "viewer")).toLowerCase(); + const hasReadAccess = isOwnedOrLocal || accessRole === "editor" || accessRole === "commenter" || accessRole === "viewer"; const canUpload = uploadEnabled && isOwnedOrLocal && isLatestVersion && (!isUploaded || !isUpToDate); const canShare = shareLinksEnabled && isOwnedOrLocal && isLatestVersion; const canManageShare = sharingEnabled && isOwnedOrLocal && Boolean(file.remoteStorageId); - const canCopyShareLink = - shareLinksEnabled && Boolean(file.remoteHasShareLinks) && Boolean(file.remoteStorageId); + const canCopyShareLink = shareLinksEnabled && Boolean(file.remoteHasShareLinks) && Boolean(file.remoteStorageId); const canDownloadFile = Boolean(onDownload) && hasReadAccess; const shareBaseUrl = useMemo(() => { - const frontendUrl = (config?.frontendUrl || '').trim(); + const frontendUrl = (config?.frontendUrl || "").trim(); if (frontendUrl) { - const normalized = frontendUrl.endsWith('/') - ? frontendUrl.slice(0, -1) - : frontendUrl; + const normalized = frontendUrl.endsWith("/") ? frontendUrl.slice(0, -1) : frontendUrl; return `${normalized}/share/`; } - return absoluteWithBasePath('/share/'); + return absoluteWithBasePath("/share/"); }, [config?.frontendUrl]); const handleCopyShareLink = useCallback(async () => { @@ -116,14 +112,14 @@ const FileListItem: React.FC = ({ try { const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>( `/api/v1/storage/files/${file.remoteStorageId}`, - { suppressErrorToast: true } as any + { suppressErrorToast: true } as any, ); const links = response.data?.shareLinks ?? []; const token = links[links.length - 1]?.token; if (!token) { alert({ - alertType: 'warning', - title: t('storageShare.noLinks', 'No active share links yet.'), + alertType: "warning", + title: t("storageShare.noLinks", "No active share links yet."), expandable: false, durationMs: 2500, }); @@ -131,16 +127,16 @@ const FileListItem: React.FC = ({ } await navigator.clipboard.writeText(`${shareBaseUrl}${token}`); alert({ - alertType: 'success', - title: t('storageShare.copied', 'Link copied to clipboard'), + alertType: "success", + title: t("storageShare.copied", "Link copied to clipboard"), expandable: false, durationMs: 2000, }); } catch (error) { - console.error('Failed to copy share link:', error); + console.error("Failed to copy share link:", error); alert({ - alertType: 'warning', - title: t('storageShare.copyFailed', 'Copy failed'), + alertType: "warning", + title: t("storageShare.copyFailed", "Copy failed"), expandable: false, durationMs: 2500, }); @@ -152,20 +148,22 @@ const FileListItem: React.FC = ({ onSelect(e.shiftKey)} onDoubleClick={onDoubleClick} @@ -186,8 +184,8 @@ const FileListItem: React.FC = ({ color={isActive ? "green" : undefined} styles={{ input: { - cursor: isActive ? 'not-allowed' : 'pointer' - } + cursor: isActive ? "not-allowed" : "pointer", + }, }} /> @@ -203,12 +201,12 @@ const FileListItem: React.FC = ({ size="xs" variant="light" style={{ - backgroundColor: 'var(--file-active-badge-bg)', - color: 'var(--file-active-badge-fg)', - border: '1px solid var(--file-active-badge-border)' + backgroundColor: "var(--file-active-badge-bg)", + color: "var(--file-active-badge-fg)", + border: "1px solid var(--file-active-badge-border)", }} > - {t('fileManager.active', 'Active')} + {t("fileManager.active", "Active")} )} @@ -216,44 +214,33 @@ const FileListItem: React.FC = ({ {sharingEnabled && isSharedWithYou ? ( - {t('fileManager.sharedWithYou', 'Shared with you')} + {t("fileManager.sharedWithYou", "Shared with you")} ) : null} - {sharingEnabled && isSharedWithYou && accessRole && accessRole !== 'editor' ? ( + {sharingEnabled && isSharedWithYou && accessRole && accessRole !== "editor" ? ( - {accessRole === 'commenter' - ? t('storageShare.roleCommenter', 'Commenter') - : t('storageShare.roleViewer', 'Viewer')} + {accessRole === "commenter" + ? t("storageShare.roleCommenter", "Commenter") + : t("storageShare.roleViewer", "Viewer")} ) : isLocalOnly ? ( - {t('fileManager.localOnly', 'Local only')} + {t("fileManager.localOnly", "Local only")} ) : uploadEnabled && isOutOfSync ? ( - } - > - {t('fileManager.changesNotUploaded', 'Changes not uploaded')} + }> + {t("fileManager.changesNotUploaded", "Changes not uploaded")} ) : uploadEnabled && isUploaded ? ( - } - > - {t('fileManager.synced', 'Synced')} + }> + {t("fileManager.synced", "Synced")} ) : null} {sharingEnabled && file.remoteOwnedByCurrentUser !== false && file.remoteHasShareLinks && ( - {t('fileManager.sharedByYou', 'Shared by you')} + {t("fileManager.sharedByYou", "Shared by you")} )} - @@ -262,12 +249,7 @@ const FileListItem: React.FC = ({ {/* Tool chain for processed files */} {file.toolHistory && file.toolHistory.length > 0 && ( - + )} @@ -288,9 +270,9 @@ const FileListItem: React.FC = ({ onClick={(e) => e.stopPropagation()} style={{ opacity: shouldShowHovered ? 1 : 0, - transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)', - transition: 'opacity 0.3s ease, transform 0.3s ease', - pointerEvents: shouldShowHovered ? 'auto' : 'none' + transform: shouldShowHovered ? "scale(1)" : "scale(0.8)", + transition: "opacity 0.3s ease, transform 0.3s ease", + pointerEvents: shouldShowHovered ? "auto" : "none", }} > @@ -308,7 +290,7 @@ const FileListItem: React.FC = ({ removeFiles([file.id]); }} > - {t('fileManager.closeFile', 'Close File')} + {t("fileManager.closeFile", "Close File")} @@ -322,7 +304,7 @@ const FileListItem: React.FC = ({ onDownload?.(); }} > - {t('fileManager.download', 'Download')} + {t("fileManager.download", "Download")} )} @@ -335,8 +317,8 @@ const FileListItem: React.FC = ({ }} > {isUploaded - ? t('fileManager.updateOnServer', 'Update on Server') - : t('fileManager.uploadToServer', 'Upload to Server')} + ? t("fileManager.updateOnServer", "Update on Server") + : t("fileManager.uploadToServer", "Upload to Server")} )} @@ -348,7 +330,7 @@ const FileListItem: React.FC = ({ setShowShareModal(true); }} > - {t('fileManager.share', 'Share')} + {t("fileManager.share", "Share")} )} @@ -360,7 +342,7 @@ const FileListItem: React.FC = ({ void handleCopyShareLink(); }} > - {t('storageShare.copyLink', 'Copy share link')} + {t("storageShare.copyLink", "Copy share link")} )} @@ -372,7 +354,7 @@ const FileListItem: React.FC = ({ setShowShareManageModal(true); }} > - {t('storageShare.manage', 'Manage sharing')} + {t("storageShare.manage", "Manage sharing")} )} @@ -380,20 +362,13 @@ const FileListItem: React.FC = ({ {isLatestVersion && hasVersionHistory && ( <> - } + leftSection={} onClick={(e) => { e.stopPropagation(); onToggleExpansion(leafFileId); }} > - { - (isExpanded ? - t('fileManager.hideHistory', 'Hide History') : - t('fileManager.showHistory', 'Show History') - ) - } + {isExpanded ? t("fileManager.hideHistory", "Hide History") : t("fileManager.showHistory", "Show History")} @@ -408,7 +383,7 @@ const FileListItem: React.FC = ({ e.stopPropagation(); }} > - {t('fileManager.restore', 'Restore')} + {t("fileManager.restore", "Restore")} @@ -424,7 +399,7 @@ const FileListItem: React.FC = ({ onUnzipFile(file); }} > - {t('fileManager.unzip', 'Unzip')} + {t("fileManager.unzip", "Unzip")} @@ -437,14 +412,13 @@ const FileListItem: React.FC = ({ onRemove(); }} > - {t('fileManager.delete', 'Delete')} + {t("fileManager.delete", "Delete")} - - { } + {} {canUpload && ( = ({ /> )} {canManageShare && ( - setShowShareManageModal(false)} - file={file} - /> + setShowShareManageModal(false)} file={file} /> )} ); diff --git a/frontend/src/core/components/fileManager/FileSourceButtons.tsx b/frontend/src/core/components/fileManager/FileSourceButtons.tsx index 8695821efd..51076ee954 100644 --- a/frontend/src/core/components/fileManager/FileSourceButtons.tsx +++ b/frontend/src/core/components/fileManager/FileSourceButtons.tsx @@ -1,15 +1,15 @@ -import React, { useState } from 'react'; -import { Stack, Text, Button, Group } from '@mantine/core'; -import HistoryIcon from '@mui/icons-material/History'; -import PhonelinkIcon from '@mui/icons-material/Phonelink'; -import { useTranslation } from 'react-i18next'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import { useGoogleDrivePicker } from '@app/hooks/useGoogleDrivePicker'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { useIsMobile } from '@app/hooks/useIsMobile'; -import MobileUploadModal from '@app/components/shared/MobileUploadModal'; +import React, { useState } from "react"; +import { Stack, Text, Button, Group } from "@mantine/core"; +import HistoryIcon from "@mui/icons-material/History"; +import PhonelinkIcon from "@mui/icons-material/Phonelink"; +import { useTranslation } from "react-i18next"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import { useGoogleDrivePicker } from "@app/hooks/useGoogleDrivePicker"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useIsMobile } from "@app/hooks/useIsMobile"; +import MobileUploadModal from "@app/components/shared/MobileUploadModal"; interface FileSourceButtonsProps { horizontal?: boolean; @@ -24,17 +24,15 @@ const GoogleDriveIcon: React.FC<{ disabled?: boolean }> = ({ disabled }) => ( src="/images/google-drive.svg" alt="Google Drive" style={{ - width: '20px', - height: '20px', + width: "20px", + height: "20px", opacity: disabled ? 0.5 : 1, - filter: disabled ? 'grayscale(100%)' : 'none', + filter: disabled ? "grayscale(100%)" : "none", }} /> ); -const FileSourceButtons: React.FC = ({ - horizontal = false -}) => { +const FileSourceButtons: React.FC = ({ horizontal = false }) => { const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect, onNewFilesSelect } = useFileManagerContext(); const { t } = useTranslation(); const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = useGoogleDrivePicker(); @@ -53,7 +51,7 @@ const FileSourceButtons: React.FC = ({ onGoogleDriveSelect(files); } } catch (error) { - console.error('Failed to pick files from Google Drive:', error); + console.error("Failed to pick files from Google Drive:", error); } }; @@ -74,18 +72,18 @@ const FileSourceButtons: React.FC = ({ const shouldHideMobileQR = !isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner; const buttonProps = { - variant: (source: string) => activeSource === source ? 'filled' : 'subtle', - getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-2)' : undefined, + variant: (source: string) => (activeSource === source ? "filled" : "subtle"), + getColor: (source: string) => (activeSource === source ? "var(--mantine-color-gray-2)" : undefined), getStyles: (source: string) => ({ root: { - backgroundColor: activeSource === source ? undefined : 'transparent', - color: activeSource === source ? 'var(--mantine-color-gray-9)' : 'var(--mantine-color-gray-6)', - border: 'none', - '&:hover': { - backgroundColor: activeSource === source ? undefined : 'var(--mantine-color-gray-0)' - } - } - }) + backgroundColor: activeSource === source ? undefined : "transparent", + color: activeSource === source ? "var(--mantine-color-gray-9)" : "var(--mantine-color-gray-6)", + border: "none", + "&:hover": { + backgroundColor: activeSource === source ? undefined : "var(--mantine-color-gray-0)", + }, + }, + }), }; const buttons = ( @@ -93,18 +91,18 @@ const FileSourceButtons: React.FC = ({ )} {!shouldHideMobileQR && ( )} @@ -178,7 +180,7 @@ const FileSourceButtons: React.FC = ({ if (horizontal) { return ( <> - + {buttons} = ({ return ( <> - - - {t('fileManager.myFiles', 'My Files')} + + + {t("fileManager.myFiles", "My Files")} {buttons} diff --git a/frontend/src/core/components/fileManager/HiddenFileInput.tsx b/frontend/src/core/components/fileManager/HiddenFileInput.tsx index 27482df519..fce23187cd 100644 --- a/frontend/src/core/components/fileManager/HiddenFileInput.tsx +++ b/frontend/src/core/components/fileManager/HiddenFileInput.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; const HiddenFileInput: React.FC = () => { const { fileInputRef, onFileInputChange } = useFileManagerContext(); @@ -10,7 +10,7 @@ const HiddenFileInput: React.FC = () => { type="file" multiple={true} onChange={onFileInputChange} - style={{ display: 'none' }} + style={{ display: "none" }} data-testid="file-input" /> ); diff --git a/frontend/src/core/components/fileManager/MobileLayout.tsx b/frontend/src/core/components/fileManager/MobileLayout.tsx index 0701874852..759fb2c7f9 100644 --- a/frontend/src/core/components/fileManager/MobileLayout.tsx +++ b/frontend/src/core/components/fileManager/MobileLayout.tsx @@ -1,19 +1,15 @@ -import React from 'react'; -import { Box } from '@mantine/core'; -import FileSourceButtons from '@app/components/fileManager/FileSourceButtons'; -import FileDetails from '@app/components/fileManager/FileDetails'; -import SearchInput from '@app/components/fileManager/SearchInput'; -import FileListArea from '@app/components/fileManager/FileListArea'; -import FileActions from '@app/components/fileManager/FileActions'; -import HiddenFileInput from '@app/components/fileManager/HiddenFileInput'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { Box } from "@mantine/core"; +import FileSourceButtons from "@app/components/fileManager/FileSourceButtons"; +import FileDetails from "@app/components/fileManager/FileDetails"; +import SearchInput from "@app/components/fileManager/SearchInput"; +import FileListArea from "@app/components/fileManager/FileListArea"; +import FileActions from "@app/components/fileManager/FileActions"; +import HiddenFileInput from "@app/components/fileManager/HiddenFileInput"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; const MobileLayout: React.FC = () => { - const { - activeSource, - selectedFiles, - modalHeight, - } = useFileManagerContext(); + const { activeSource, selectedFiles, modalHeight } = useFileManagerContext(); // Calculate the height more accurately based on actual content const calculateFileListHeight = () => { @@ -21,17 +17,17 @@ const MobileLayout: React.FC = () => { const baseHeight = `calc(${modalHeight} - 2rem)`; // Account for Stack padding // Estimate heights of fixed components - const fileSourceHeight = '3rem'; // FileSourceButtons height - const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height - const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom) - const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height - const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps + const fileSourceHeight = "3rem"; // FileSourceButtons height + const fileDetailsHeight = selectedFiles.length > 0 ? "10rem" : "8rem"; // FileDetails compact height + const fileActionsHeight = activeSource === "recent" ? "3rem" : "0rem"; // FileActions height (now at bottom) + const searchHeight = activeSource === "recent" ? "3rem" : "0rem"; // SearchInput height + const gapHeight = activeSource === "recent" ? "3.75rem" : "2rem"; // Stack gaps return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`; }; return ( - + {/* Section 1: File Sources - Fixed at top */} @@ -42,28 +38,34 @@ const MobileLayout: React.FC = () => { {/* Section 3 & 4: Search Bar + File List - Unified background extending to modal edge */} - - {activeSource === 'recent' && ( + + {activeSource === "recent" && ( <> - + - + @@ -74,11 +76,11 @@ const MobileLayout: React.FC = () => { scrollAreaHeight={calculateFileListHeight()} scrollAreaStyle={{ height: calculateFileListHeight(), - maxHeight: '60vh', - minHeight: '9.375rem', - backgroundColor: 'transparent', - border: 'none', - borderRadius: 0 + maxHeight: "60vh", + minHeight: "9.375rem", + backgroundColor: "transparent", + border: "none", + borderRadius: 0, }} /> diff --git a/frontend/src/core/components/fileManager/SearchInput.tsx b/frontend/src/core/components/fileManager/SearchInput.tsx index 2b318604c6..b7dbf9306c 100644 --- a/frontend/src/core/components/fileManager/SearchInput.tsx +++ b/frontend/src/core/components/fileManager/SearchInput.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { TextInput } from '@mantine/core'; -import SearchIcon from '@mui/icons-material/Search'; -import { useTranslation } from 'react-i18next'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { TextInput } from "@mantine/core"; +import SearchIcon from "@mui/icons-material/Search"; +import { useTranslation } from "react-i18next"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; interface SearchInputProps { style?: React.CSSProperties; @@ -14,20 +14,19 @@ const SearchInput: React.FC = ({ style }) => { return ( } value={searchTerm} onChange={(e) => onSearchChange(e.target.value)} - - style={{ padding: '0.5rem', ...style }} + style={{ padding: "0.5rem", ...style }} styles={{ input: { - border: 'none', - backgroundColor: 'transparent' - } + border: "none", + backgroundColor: "transparent", + }, }} /> ); }; -export default SearchInput; \ No newline at end of file +export default SearchInput; diff --git a/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx b/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx index 7f6d9f26dd..357b4a2f4a 100644 --- a/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx +++ b/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx @@ -1,29 +1,29 @@ -import React from 'react'; -import { HotkeyBinding } from '@app/utils/hotkeys'; -import { useHotkeys } from '@app/contexts/HotkeyContext'; +import React from "react"; +import { HotkeyBinding } from "@app/utils/hotkeys"; +import { useHotkeys } from "@app/contexts/HotkeyContext"; interface HotkeyDisplayProps { binding: HotkeyBinding | null | undefined; - size?: 'sm' | 'md'; + size?: "sm" | "md"; muted?: boolean; } const baseKeyStyle: React.CSSProperties = { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - borderRadius: '0.375rem', - background: 'var(--mantine-color-gray-1)', - border: '1px solid var(--mantine-color-gray-3)', - padding: '0.125rem 0.35rem', - fontSize: '0.75rem', + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + borderRadius: "0.375rem", + background: "var(--mantine-color-gray-1)", + border: "1px solid var(--mantine-color-gray-3)", + padding: "0.125rem 0.35rem", + fontSize: "0.75rem", lineHeight: 1, - fontFamily: 'var(--mantine-font-family-monospace, monospace)', - minWidth: '1.35rem', - color: 'var(--mantine-color-text)', + fontFamily: "var(--mantine-font-family-monospace, monospace)", + minWidth: "1.35rem", + color: "var(--mantine-color-text)", }; -export const HotkeyDisplay: React.FC = ({ binding, size = 'sm', muted = false }) => { +export const HotkeyDisplay: React.FC = ({ binding, size = "sm", muted = false }) => { const { getDisplayParts } = useHotkeys(); const parts = getDisplayParts(binding); @@ -31,24 +31,26 @@ export const HotkeyDisplay: React.FC = ({ binding, size = 's return null; } - const keyStyle = size === 'md' - ? { ...baseKeyStyle, fontSize: '0.85rem', padding: '0.2rem 0.5rem' } - : baseKeyStyle; + const keyStyle = size === "md" ? { ...baseKeyStyle, fontSize: "0.85rem", padding: "0.2rem 0.5rem" } : baseKeyStyle; return ( {parts.map((part, index) => ( {part} - {index < parts.length - 1 && +} + {index < parts.length - 1 && ( + + + + + )} ))} diff --git a/frontend/src/core/components/layout/Workbench.tsx b/frontend/src/core/components/layout/Workbench.tsx index 521ac5db15..89c04dc670 100644 --- a/frontend/src/core/components/layout/Workbench.tsx +++ b/frontend/src/core/components/layout/Workbench.tsx @@ -1,24 +1,24 @@ -import { useCallback } from 'react'; -import { Box } from '@mantine/core'; -import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useFileHandler } from '@app/hooks/useFileHandler'; -import { useFileState, useFileActions } from '@app/contexts/FileContext'; -import { useNavigationState, useNavigationActions, useNavigationGuard } from '@app/contexts/NavigationContext'; -import { isBaseWorkbench } from '@app/types/workbench'; -import { useViewer } from '@app/contexts/ViewerContext'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { FileId } from '@app/types/file'; -import styles from '@app/components/layout/Workbench.module.css'; +import { useCallback } from "react"; +import { Box } from "@mantine/core"; +import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useFileHandler } from "@app/hooks/useFileHandler"; +import { useFileState, useFileActions } from "@app/contexts/FileContext"; +import { useNavigationState, useNavigationActions, useNavigationGuard } from "@app/contexts/NavigationContext"; +import { isBaseWorkbench } from "@app/types/workbench"; +import { useViewer } from "@app/contexts/ViewerContext"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { FileId } from "@app/types/file"; +import styles from "@app/components/layout/Workbench.module.css"; -import TopControls from '@app/components/shared/TopControls'; -import FileEditor from '@app/components/fileEditor/FileEditor'; -import PageEditor from '@app/components/pageEditor/PageEditor'; -import PageEditorControls from '@app/components/pageEditor/PageEditorControls'; -import Viewer from '@app/components/viewer/Viewer'; -import LandingPage from '@app/components/shared/LandingPage'; -import Footer from '@app/components/shared/Footer'; -import DismissAllErrorsButton from '@app/components/shared/DismissAllErrorsButton'; +import TopControls from "@app/components/shared/TopControls"; +import FileEditor from "@app/components/fileEditor/FileEditor"; +import PageEditor from "@app/components/pageEditor/PageEditor"; +import PageEditorControls from "@app/components/pageEditor/PageEditorControls"; +import Viewer from "@app/components/viewer/Viewer"; +import LandingPage from "@app/components/shared/LandingPage"; +import Footer from "@app/components/shared/Footer"; +import DismissAllErrorsButton from "@app/components/shared/DismissAllErrorsButton"; // No props needed - component uses contexts directly export default function Workbench() { @@ -54,41 +54,47 @@ export default function Workbench() { // Get active file index from ViewerContext const { activeFileIndex, setActiveFileIndex } = useViewer(); - + // Get navigation guard for unsaved changes check when switching files const { requestNavigation } = useNavigationGuard(); // Wrap file selection to check for unsaved changes before switching // requestNavigation will show the modal if there are unsaved changes, otherwise navigate immediately - const handleFileSelect = useCallback((index: number) => { - // Don't do anything if selecting the same file - if (index === activeFileIndex) return; + const handleFileSelect = useCallback( + (index: number) => { + // Don't do anything if selecting the same file + if (index === activeFileIndex) return; - // requestNavigation handles the unsaved changes check internally - requestNavigation(() => { - setActiveFileIndex(index); - }); - }, [activeFileIndex, requestNavigation, setActiveFileIndex]); + // requestNavigation handles the unsaved changes check internally + requestNavigation(() => { + setActiveFileIndex(index); + }); + }, + [activeFileIndex, requestNavigation, setActiveFileIndex], + ); - const handleFileRemove = useCallback(async (fileId: FileId) => { - await fileActions.removeFiles([fileId], false); // false = don't delete from IndexedDB, just remove from context - }, [fileActions]); + const handleFileRemove = useCallback( + async (fileId: FileId) => { + await fileActions.removeFiles([fileId], false); // false = don't delete from IndexedDB, just remove from context + }, + [fileActions], + ); const handlePreviewClose = () => { setPreviewFile(null); - const previousMode = sessionStorage.getItem('previousMode'); - if (previousMode === 'split') { + const previousMode = sessionStorage.getItem("previousMode"); + if (previousMode === "split") { // Use context's handleToolSelect which coordinates tool selection and view changes - handleToolSelect('split'); - sessionStorage.removeItem('previousMode'); - } else if (previousMode === 'compress') { - handleToolSelect('compress'); - sessionStorage.removeItem('previousMode'); - } else if (previousMode === 'convert') { - handleToolSelect('convert'); - sessionStorage.removeItem('previousMode'); + handleToolSelect("split"); + sessionStorage.removeItem("previousMode"); + } else if (previousMode === "compress") { + handleToolSelect("compress"); + sessionStorage.removeItem("previousMode"); + } else if (previousMode === "convert") { + handleToolSelect("convert"); + sessionStorage.removeItem("previousMode"); } else { - setCurrentView('fileEditor'); + setCurrentView("fileEditor"); } }; @@ -104,15 +110,11 @@ export default function Workbench() { } if (activeFiles.length === 0) { - return ( - - ); + return ; } switch (currentView) { case "fileEditor": - return ( { addFiles(filesToMerge); setCurrentView("viewer"); - } + }, })} /> ); case "viewer": - return ( - +
+ {pageEditorFunctions && ( -
+
+ onClosePdf={pageEditorFunctions.closePdf} + onUndo={pageEditorFunctions.handleUndo} + onRedo={pageEditorFunctions.handleRedo} + canUndo={pageEditorFunctions.canUndo} + canRedo={pageEditorFunctions.canRedo} + onRotate={pageEditorFunctions.handleRotate} + onDelete={pageEditorFunctions.handleDelete} + onSplit={pageEditorFunctions.handleSplit} + onSplitAll={pageEditorFunctions.handleSplitAll} + onPageBreak={pageEditorFunctions.handlePageBreak} + onPageBreakAll={pageEditorFunctions.handlePageBreakAll} + onExportAll={pageEditorFunctions.onExportAll} + exportLoading={pageEditorFunctions.exportLoading} + selectionMode={pageEditorFunctions.selectionMode} + selectedPageIds={pageEditorFunctions.selectedPageIds} + displayDocument={pageEditorFunctions.displayDocument} + splitPositions={pageEditorFunctions.splitPositions} + totalPages={pageEditorFunctions.totalPages} + />
)}
@@ -188,16 +186,16 @@ export default function Workbench() { style={ isRainbowMode ? {} // No background color in rainbow mode - : { backgroundColor: 'var(--bg-background)' } + : { backgroundColor: "var(--bg-background)" } } > {/* Top Controls */} - {activeFiles.length > 0 && !customWorkbenchViews.find(v => v.workbenchId === currentView)?.hideTopControls && ( + {activeFiles.length > 0 && !customWorkbenchViews.find((v) => v.workbenchId === currentView)?.hideTopControls && ( { + activeFiles={activeFiles.map((f) => { const stub = selectors.getStirlingFileStub(f.fileId); return { fileId: f.fileId, name: f.name, versionNumber: stub?.versionNumber }; })} @@ -212,10 +210,10 @@ export default function Workbench() { {/* Main content area */} {renderMainContent()} diff --git a/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css b/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css index c507653092..5b3aec2d91 100644 --- a/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css +++ b/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css @@ -118,7 +118,6 @@ } } - .heroIconsContainer { display: flex; gap: 32px; @@ -141,7 +140,9 @@ border: none; padding: 0; cursor: pointer; - transition: transform 0.2s ease, opacity 0.2s ease; + transition: + transform 0.2s ease, + opacity 0.2s ease; display: flex; align-items: center; justify-content: center; @@ -181,7 +182,14 @@ } .iconLabel { - font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + Roboto, + Arial, + sans-serif; font-size: 14px; font-weight: 500; color: rgba(255, 255, 255, 0.9); @@ -266,7 +274,7 @@ opacity: 1; border: 1px solid rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9); - color: #1F2933; + color: #1f2933; box-shadow: 0 0 8px rgba(255, 255, 255, 0.7); } @@ -282,7 +290,14 @@ /* Title styles */ .titleText { - font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + Roboto, + Arial, + sans-serif; font-weight: 600; font-size: 22px; color: var(--onboarding-title); @@ -290,7 +305,14 @@ /* Body text styles */ .bodyText { - font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + Roboto, + Arial, + sans-serif; font-size: 16px; color: var(--onboarding-body); line-height: 1.5; @@ -314,8 +336,8 @@ } .v2Badge { - background: #DBEFFF; - color: #2A4BFF; + background: #dbefff; + color: #2a4bff; padding: 4px 12px; border-radius: 6px; font-size: 14px; diff --git a/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx b/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx index c50ae7b43e..e0a71a3e69 100644 --- a/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx +++ b/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import { Button, Group, ActionIcon } from '@mantine/core'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import { useTranslation } from 'react-i18next'; -import { ButtonDefinition, type FlowState } from '@app/components/onboarding/onboardingFlowConfig'; -import type { LicenseNotice } from '@app/types/types'; -import type { ButtonAction } from '@app/components/onboarding/onboardingFlowConfig'; +import React from "react"; +import { Button, Group, ActionIcon } from "@mantine/core"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import { useTranslation } from "react-i18next"; +import { ButtonDefinition, type FlowState } from "@app/components/onboarding/onboardingFlowConfig"; +import type { LicenseNotice } from "@app/types/types"; +import type { ButtonAction } from "@app/components/onboarding/onboardingFlowConfig"; interface SlideButtonsProps { slideDefinition: { @@ -18,49 +18,49 @@ interface SlideButtonsProps { export function SlideButtons({ slideDefinition, licenseNotice, flowState, onAction }: SlideButtonsProps) { const { t } = useTranslation(); - const leftButtons = slideDefinition.buttons.filter((btn) => btn.group === 'left'); - const rightButtons = slideDefinition.buttons.filter((btn) => btn.group === 'right'); + const leftButtons = slideDefinition.buttons.filter((btn) => btn.group === "left"); + const rightButtons = slideDefinition.buttons.filter((btn) => btn.group === "right"); - const buttonStyles = (variant: ButtonDefinition['variant']) => - variant === 'primary' + const buttonStyles = (variant: ButtonDefinition["variant"]) => + variant === "primary" ? { root: { - background: 'var(--onboarding-primary-button-bg)', - color: 'var(--onboarding-primary-button-text)', + background: "var(--onboarding-primary-button-bg)", + color: "var(--onboarding-primary-button-text)", }, } : { root: { - background: 'var(--onboarding-secondary-button-bg)', - border: '1px solid var(--onboarding-secondary-button-border)', - color: 'var(--onboarding-secondary-button-text)', + background: "var(--onboarding-secondary-button-bg)", + border: "1px solid var(--onboarding-secondary-button-border)", + color: "var(--onboarding-secondary-button-text)", }, }; const resolveButtonLabel = (button: ButtonDefinition) => { // Special case: override "See Plans" with "Upgrade now" when over limit if ( - button.type === 'button' && - slideDefinition.id === 'server-license' && - button.action === 'see-plans' && + button.type === "button" && + slideDefinition.id === "server-license" && + button.action === "see-plans" && licenseNotice.isOverLimit ) { - return t('onboarding.serverLicense.upgrade', 'Upgrade now →'); + return t("onboarding.serverLicense.upgrade", "Upgrade now →"); } // Translate the label (it's a translation key) - const label = button.label ?? ''; - if (!label) return ''; + const label = button.label ?? ""; + if (!label) return ""; // Extract fallback text from translation key (e.g., 'onboarding.buttons.next' -> 'Next') - const fallback = label.split('.').pop() || label; + const fallback = label.split(".").pop() || label; return t(label, fallback); }; const renderButton = (button: ButtonDefinition) => { const disabled = button.disabledWhen?.(flowState) ?? false; - if (button.type === 'icon') { + if (button.type === "icon") { return ( - {button.icon === 'chevron-left' && } + {button.icon === "chevron-left" && } ); } - const variant = button.variant ?? 'secondary'; + const variant = button.variant ?? "secondary"; const label = resolveButtonLabel(button); return ( diff --git a/frontend/src/core/components/onboarding/Onboarding.tsx b/frontend/src/core/components/onboarding/Onboarding.tsx index d098a801d5..e62ae9cedf 100644 --- a/frontend/src/core/components/onboarding/Onboarding.tsx +++ b/frontend/src/core/components/onboarding/Onboarding.tsx @@ -1,33 +1,30 @@ -import { useEffect, useMemo, useCallback, useState } from 'react'; -import { type StepType } from '@reactour/tour'; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { isAuthRoute } from '@app/constants/routes'; -import { dispatchTourState } from '@app/constants/events'; -import { useOnboardingOrchestrator } from '@app/components/onboarding/orchestrator/useOnboardingOrchestrator'; -import { useBypassOnboarding } from '@app/components/onboarding/useBypassOnboarding'; -import OnboardingTour, { type AdvanceArgs, type CloseArgs } from '@app/components/onboarding/OnboardingTour'; -import OnboardingModalSlide from '@app/components/onboarding/OnboardingModalSlide'; -import { - useServerLicenseRequest, - useTourRequest, -} from '@app/components/onboarding/useOnboardingEffects'; -import { useOnboardingDownload } from '@app/components/onboarding/useOnboardingDownload'; -import { SLIDE_DEFINITIONS, type SlideId, type ButtonAction } from '@app/components/onboarding/onboardingFlowConfig'; -import ToolPanelModePrompt from '@app/components/tools/ToolPanelModePrompt'; -import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext'; -import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext'; -import { createUserStepsConfig } from '@app/components/onboarding/userStepsConfig'; -import { createAdminStepsConfig } from '@app/components/onboarding/adminStepsConfig'; -import { createWhatsNewStepsConfig } from '@app/components/onboarding/whatsNewStepsConfig'; -import { removeAllGlows } from '@app/components/onboarding/tourGlow'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useServerExperience } from '@app/hooks/useServerExperience'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import apiClient from '@app/services/apiClient'; -import '@app/components/onboarding/OnboardingTour.css'; -import { useAccountLogout } from '@app/extensions/accountLogout'; -import { useAuth } from '@app/auth/UseSession'; +import { useEffect, useMemo, useCallback, useState } from "react"; +import { type StepType } from "@reactour/tour"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useLocation } from "react-router-dom"; +import { isAuthRoute } from "@app/constants/routes"; +import { dispatchTourState } from "@app/constants/events"; +import { useOnboardingOrchestrator } from "@app/components/onboarding/orchestrator/useOnboardingOrchestrator"; +import { useBypassOnboarding } from "@app/components/onboarding/useBypassOnboarding"; +import OnboardingTour, { type AdvanceArgs, type CloseArgs } from "@app/components/onboarding/OnboardingTour"; +import OnboardingModalSlide from "@app/components/onboarding/OnboardingModalSlide"; +import { useServerLicenseRequest, useTourRequest } from "@app/components/onboarding/useOnboardingEffects"; +import { useOnboardingDownload } from "@app/components/onboarding/useOnboardingDownload"; +import { SLIDE_DEFINITIONS, type SlideId, type ButtonAction } from "@app/components/onboarding/onboardingFlowConfig"; +import ToolPanelModePrompt from "@app/components/tools/ToolPanelModePrompt"; +import { useTourOrchestration } from "@app/contexts/TourOrchestrationContext"; +import { useAdminTourOrchestration } from "@app/contexts/AdminTourOrchestrationContext"; +import { createUserStepsConfig } from "@app/components/onboarding/userStepsConfig"; +import { createAdminStepsConfig } from "@app/components/onboarding/adminStepsConfig"; +import { createWhatsNewStepsConfig } from "@app/components/onboarding/whatsNewStepsConfig"; +import { removeAllGlows } from "@app/components/onboarding/tourGlow"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useServerExperience } from "@app/hooks/useServerExperience"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import apiClient from "@app/services/apiClient"; +import "@app/components/onboarding/OnboardingTour.css"; +import { useAccountLogout } from "@app/extensions/accountLogout"; +import { useAuth } from "@app/auth/UseSession"; export default function Onboarding() { const { t } = useTranslation(); @@ -52,13 +49,16 @@ export default function Onboarding() { const accountLogout = useAccountLogout(); const { signOut } = useAuth(); - const handleRoleSelect = useCallback((role: 'admin' | 'user' | null) => { - actions.updateRuntimeState({ selectedRole: role }); - serverExperience.setSelfReportedAdmin(role === 'admin'); - }, [actions, serverExperience]); + const handleRoleSelect = useCallback( + (role: "admin" | "user" | null) => { + actions.updateRuntimeState({ selectedRole: role }); + serverExperience.setSelfReportedAdmin(role === "admin"); + }, + [actions, serverExperience], + ); const redirectToLogin = useCallback(() => { - window.location.assign('/login'); + window.location.assign("/login"); }, []); const handlePasswordChanged = useCallback(async () => { @@ -80,84 +80,97 @@ export default function Onboarding() { } }, [isLoading, analyticsModalDismissed, serverExperience.effectiveIsAdmin, config?.enableAnalytics]); - const handleAnalyticsChoice = useCallback(async (enableAnalytics: boolean) => { - if (analyticsLoading) return; - setAnalyticsLoading(true); - setAnalyticsError(null); + const handleAnalyticsChoice = useCallback( + async (enableAnalytics: boolean) => { + if (analyticsLoading) return; + setAnalyticsLoading(true); + setAnalyticsError(null); - const formData = new FormData(); - formData.append('enabled', enableAnalytics.toString()); + const formData = new FormData(); + formData.append("enabled", enableAnalytics.toString()); - try { - await apiClient.post('/api/v1/settings/update-enable-analytics', formData); - await refetchConfig(); - setShowAnalyticsModal(false); - setAnalyticsModalDismissed(true); - } catch (error) { - setAnalyticsError(error instanceof Error ? error.message : 'Unknown error'); - } finally { - setAnalyticsLoading(false); - } - }, [analyticsLoading, refetchConfig]); - - const handleButtonAction = useCallback(async (action: ButtonAction) => { - switch (action) { - case 'next': - case 'complete-close': - actions.complete(); - break; - case 'prev': - actions.prev(); - break; - case 'close': - actions.skip(); - break; - case 'download-selected': - handleDownloadSelected(); - actions.complete(); - break; - case 'security-next': - if (!runtimeState.selectedRole) return; - if (runtimeState.selectedRole !== 'admin') { - actions.updateRuntimeState({ tourType: 'whatsnew' }); - setIsTourOpen(true); - } - actions.complete(); - break; - case 'launch-admin': - actions.updateRuntimeState({ tourType: 'admin' }); - setIsTourOpen(true); - break; - case 'launch-tools': - actions.updateRuntimeState({ tourType: 'whatsnew' }); - setIsTourOpen(true); - break; - case 'launch-auto': { - const tourType = serverExperience.effectiveIsAdmin || runtimeState.selectedRole === 'admin' ? 'admin' : 'whatsnew'; - actions.updateRuntimeState({ tourType }); - setIsTourOpen(true); - break; + try { + await apiClient.post("/api/v1/settings/update-enable-analytics", formData); + await refetchConfig(); + setShowAnalyticsModal(false); + setAnalyticsModalDismissed(true); + } catch (error) { + setAnalyticsError(error instanceof Error ? error.message : "Unknown error"); + } finally { + setAnalyticsLoading(false); } - case 'skip-to-license': - actions.complete(); - break; - case 'skip-tour': - actions.complete(); - break; - case 'see-plans': - actions.complete(); - navigate('/settings/adminPlan'); - break; - case 'enable-analytics': - await handleAnalyticsChoice(true); - break; - case 'disable-analytics': - await handleAnalyticsChoice(false); - break; - } - }, [actions, handleAnalyticsChoice, handleDownloadSelected, navigate, runtimeState.selectedRole, serverExperience.effectiveIsAdmin]); + }, + [analyticsLoading, refetchConfig], + ); - const isRTL = typeof document !== 'undefined' ? document.documentElement.dir === 'rtl' : false; + const handleButtonAction = useCallback( + async (action: ButtonAction) => { + switch (action) { + case "next": + case "complete-close": + actions.complete(); + break; + case "prev": + actions.prev(); + break; + case "close": + actions.skip(); + break; + case "download-selected": + handleDownloadSelected(); + actions.complete(); + break; + case "security-next": + if (!runtimeState.selectedRole) return; + if (runtimeState.selectedRole !== "admin") { + actions.updateRuntimeState({ tourType: "whatsnew" }); + setIsTourOpen(true); + } + actions.complete(); + break; + case "launch-admin": + actions.updateRuntimeState({ tourType: "admin" }); + setIsTourOpen(true); + break; + case "launch-tools": + actions.updateRuntimeState({ tourType: "whatsnew" }); + setIsTourOpen(true); + break; + case "launch-auto": { + const tourType = serverExperience.effectiveIsAdmin || runtimeState.selectedRole === "admin" ? "admin" : "whatsnew"; + actions.updateRuntimeState({ tourType }); + setIsTourOpen(true); + break; + } + case "skip-to-license": + actions.complete(); + break; + case "skip-tour": + actions.complete(); + break; + case "see-plans": + actions.complete(); + navigate("/settings/adminPlan"); + break; + case "enable-analytics": + await handleAnalyticsChoice(true); + break; + case "disable-analytics": + await handleAnalyticsChoice(false); + break; + } + }, + [ + actions, + handleAnalyticsChoice, + handleDownloadSelected, + navigate, + runtimeState.selectedRole, + serverExperience.effectiveIsAdmin, + ], + ); + + const isRTL = typeof document !== "undefined" ? document.documentElement.dir === "rtl" : false; const [isTourOpen, setIsTourOpen] = useState(false); useEffect(() => dispatchTourState(isTourOpen), [isTourOpen]); @@ -167,60 +180,63 @@ export default function Onboarding() { const adminTourOrch = useAdminTourOrchestration(); const userStepsConfig = useMemo( - () => createUserStepsConfig({ - t, - actions: { - saveWorkbenchState: tourOrch.saveWorkbenchState, - closeFilesModal, - backToAllTools: tourOrch.backToAllTools, - selectCropTool: tourOrch.selectCropTool, - loadSampleFile: tourOrch.loadSampleFile, - switchToActiveFiles: tourOrch.switchToActiveFiles, - pinFile: tourOrch.pinFile, - modifyCropSettings: tourOrch.modifyCropSettings, - executeTool: tourOrch.executeTool, - openFilesModal, - }, - }), - [t, tourOrch, closeFilesModal, openFilesModal] + () => + createUserStepsConfig({ + t, + actions: { + saveWorkbenchState: tourOrch.saveWorkbenchState, + closeFilesModal, + backToAllTools: tourOrch.backToAllTools, + selectCropTool: tourOrch.selectCropTool, + loadSampleFile: tourOrch.loadSampleFile, + switchToActiveFiles: tourOrch.switchToActiveFiles, + pinFile: tourOrch.pinFile, + modifyCropSettings: tourOrch.modifyCropSettings, + executeTool: tourOrch.executeTool, + openFilesModal, + }, + }), + [t, tourOrch, closeFilesModal, openFilesModal], ); const whatsNewStepsConfig = useMemo( - () => createWhatsNewStepsConfig({ - t, - actions: { - saveWorkbenchState: tourOrch.saveWorkbenchState, - closeFilesModal, - backToAllTools: tourOrch.backToAllTools, - openFilesModal, - loadSampleFile: tourOrch.loadSampleFile, - switchToViewer: tourOrch.switchToViewer, - switchToPageEditor: tourOrch.switchToPageEditor, - switchToActiveFiles: tourOrch.switchToActiveFiles, - selectFirstFile: tourOrch.selectFirstFile, - }, - }), - [t, tourOrch, closeFilesModal, openFilesModal] + () => + createWhatsNewStepsConfig({ + t, + actions: { + saveWorkbenchState: tourOrch.saveWorkbenchState, + closeFilesModal, + backToAllTools: tourOrch.backToAllTools, + openFilesModal, + loadSampleFile: tourOrch.loadSampleFile, + switchToViewer: tourOrch.switchToViewer, + switchToPageEditor: tourOrch.switchToPageEditor, + switchToActiveFiles: tourOrch.switchToActiveFiles, + selectFirstFile: tourOrch.selectFirstFile, + }, + }), + [t, tourOrch, closeFilesModal, openFilesModal], ); const adminStepsConfig = useMemo( - () => createAdminStepsConfig({ - t, - actions: { - saveAdminState: adminTourOrch.saveAdminState, - openConfigModal: adminTourOrch.openConfigModal, - navigateToSection: adminTourOrch.navigateToSection, - scrollNavToSection: adminTourOrch.scrollNavToSection, - }, - }), - [t, adminTourOrch] + () => + createAdminStepsConfig({ + t, + actions: { + saveAdminState: adminTourOrch.saveAdminState, + openConfigModal: adminTourOrch.openConfigModal, + navigateToSection: adminTourOrch.navigateToSection, + scrollNavToSection: adminTourOrch.scrollNavToSection, + }, + }), + [t, adminTourOrch], ); const tourSteps = useMemo(() => { switch (runtimeState.tourType) { - case 'admin': + case "admin": return Object.values(adminStepsConfig); - case 'whatsnew': + case "whatsnew": return Object.values(whatsNewStepsConfig); default: return Object.values(userStepsConfig); @@ -242,8 +258,8 @@ export default function Onboarding() { // Handle first-login password change modal useEffect(() => { - if(runtimeState.requiresPasswordChange === true) { - console.log('[Onboarding] User requires password change on first login.'); + if (runtimeState.requiresPasswordChange === true) { + console.log("[Onboarding] User requires password change on first login."); setFirstLoginModalOpen(true); } else { setFirstLoginModalOpen(false); @@ -252,18 +268,18 @@ export default function Onboarding() { // Handle MFA setup modal useEffect(() => { - if(runtimeState.requiresMfaSetup === true) { - console.log('[Onboarding] User requires MFA setup.'); + if (runtimeState.requiresMfaSetup === true) { + console.log("[Onboarding] User requires MFA setup."); setMfaModalOpen(true); } else { - console.log('[Onboarding] User does not require MFA setup.'); + console.log("[Onboarding] User does not require MFA setup."); setMfaModalOpen(false); } }, [runtimeState.requiresMfaSetup]); const finishTour = useCallback(() => { setIsTourOpen(false); - if (runtimeState.tourType === 'admin') { + if (runtimeState.tourType === "admin") { adminTourOrch.restoreAdminState(); } else { tourOrch.restoreWorkbenchState(); @@ -272,23 +288,29 @@ export default function Onboarding() { actions.complete(); }, [actions, adminTourOrch, runtimeState.tourType, tourOrch]); - const handleAdvanceTour = useCallback((args: AdvanceArgs) => { - const { setCurrentStep, currentStep: tourCurrentStep, steps, setIsOpen } = args; - if (steps && tourCurrentStep === steps.length - 1) { - setIsOpen(false); - finishTour(); - } else if (steps) { - setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1)); - } - }, [finishTour]); + const handleAdvanceTour = useCallback( + (args: AdvanceArgs) => { + const { setCurrentStep, currentStep: tourCurrentStep, steps, setIsOpen } = args; + if (steps && tourCurrentStep === steps.length - 1) { + setIsOpen(false); + finishTour(); + } else if (steps) { + setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1)); + } + }, + [finishTour], + ); - const handleCloseTour = useCallback((args: CloseArgs) => { - args.setIsOpen(false); - finishTour(); - }, [finishTour]); + const handleCloseTour = useCallback( + (args: CloseArgs) => { + args.setIsOpen(false); + finishTour(); + }, + [finishTour], + ); const currentSlideDefinition = useMemo(() => { - if (!currentStep || currentStep.type !== 'modal-slide' || !currentStep.slideId) { + if (!currentStep || currentStep.type !== "modal-slide" || !currentStep.slideId) { return null; } return SLIDE_DEFINITIONS[currentStep.slideId as SlideId]; @@ -312,15 +334,29 @@ export default function Onboarding() { analyticsLoading, onMfaSetupComplete: handleMfaSetupComplete, }); - }, [analyticsError, analyticsLoading, currentSlideDefinition, osInfo, osOptions, runtimeState.selectedRole, runtimeState.licenseNotice, handleRoleSelect, serverExperience.loginEnabled, setSelectedDownloadUrl, runtimeState.firstLoginUsername, handlePasswordChanged, handleMfaSetupComplete]); + }, [ + analyticsError, + analyticsLoading, + currentSlideDefinition, + osInfo, + osOptions, + runtimeState.selectedRole, + runtimeState.licenseNotice, + handleRoleSelect, + serverExperience.loginEnabled, + setSelectedDownloadUrl, + runtimeState.firstLoginUsername, + handlePasswordChanged, + handleMfaSetupComplete, + ]); const modalSlideCount = useMemo(() => { - return activeFlow.filter((step) => step.type === 'modal-slide').length; + return activeFlow.filter((step) => step.type === "modal-slide").length; }, [activeFlow]); const currentModalSlideIndex = useMemo(() => { - if (!currentStep || currentStep.type !== 'modal-slide') return 0; - const modalSlides = activeFlow.filter((step) => step.type === 'modal-slide'); + if (!currentStep || currentStep.type !== "modal-slide") return 0; + const modalSlides = activeFlow.filter((step) => step.type === "modal-slide"); return modalSlides.findIndex((step) => step.id === currentStep.id); }, [activeFlow, currentStep]); @@ -334,10 +370,10 @@ export default function Onboarding() { // Show analytics modal before onboarding if needed if (showAnalyticsModal) { - const slideDefinition = SLIDE_DEFINITIONS['analytics-choice']; + const slideDefinition = SLIDE_DEFINITIONS["analytics-choice"]; const slideContent = slideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", selectedRole: null, onRoleSelect: () => {}, analyticsError, @@ -353,9 +389,9 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={() => {}} // No skip allowed onAction={async (action) => { - if (action === 'enable-analytics') { + if (action === "enable-analytics") { await handleAnalyticsChoice(true); - } else if (action === 'disable-analytics') { + } else if (action === "disable-analytics") { await handleAnalyticsChoice(false); } }} @@ -365,10 +401,10 @@ export default function Onboarding() { } if (firstLoginModalOpen) { - const baseSlideDefinition = SLIDE_DEFINITIONS['first-login']; + const baseSlideDefinition = SLIDE_DEFINITIONS["first-login"]; const slideContent = baseSlideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", selectedRole: null, onRoleSelect: () => {}, firstLoginUsername: runtimeState.firstLoginUsername, @@ -385,7 +421,7 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={() => {}} onAction={async (action) => { - if (action === 'complete-close') { + if (action === "complete-close") { handlePasswordChanged(); } }} @@ -395,11 +431,11 @@ export default function Onboarding() { } if (mfaModalOpen) { - console.log('[Onboarding] Rendering MFA setup modal slide.'); - const baseSlideDefinition = SLIDE_DEFINITIONS['mfa-setup']; + console.log("[Onboarding] Rendering MFA setup modal slide."); + const baseSlideDefinition = SLIDE_DEFINITIONS["mfa-setup"]; const slideContent = baseSlideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", selectedRole: null, onRoleSelect: () => {}, onMfaSetupComplete: handleMfaSetupComplete, @@ -414,7 +450,7 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={() => {}} onAction={async (action) => { - if (action === 'complete-close') { + if (action === "complete-close") { handleMfaSetupComplete(); } }} @@ -424,16 +460,16 @@ export default function Onboarding() { } if (showLicenseSlide) { - const baseSlideDefinition = SLIDE_DEFINITIONS['server-license']; + const baseSlideDefinition = SLIDE_DEFINITIONS["server-license"]; // Remove back button for external license notice const slideDefinition = { ...baseSlideDefinition, - buttons: baseSlideDefinition.buttons.filter(btn => btn.key !== 'license-back') + buttons: baseSlideDefinition.buttons.filter((btn) => btn.key !== "license-back"), }; const effectiveLicenseNotice = externalLicenseNotice || runtimeState.licenseNotice; const slideContent = slideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", osOptions: [], onDownloadUrlChange: () => {}, selectedRole: null, @@ -451,9 +487,9 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={closeLicenseSlide} onAction={(action) => { - if (action === 'see-plans') { + if (action === "see-plans") { closeLicenseSlide(); - navigate('/settings/adminPlan'); + navigate("/settings/adminPlan"); } else { closeLicenseSlide(); } @@ -487,10 +523,10 @@ export default function Onboarding() { // Render the current onboarding step switch (currentStep.type) { - case 'tool-prompt': + case "tool-prompt": return ; - case 'modal-slide': + case "modal-slide": if (!currentSlideDefinition || !currentSlideContent) return null; return ( { - if (slideDefinition.hero.type === 'dual-icon') { + if (slideDefinition.hero.type === "dual-icon") { return (
@@ -56,20 +55,20 @@ export default function OnboardingModalSlide({ return (
- {slideDefinition.hero.type === 'rocket' && ( + {slideDefinition.hero.type === "rocket" && ( )} - {slideDefinition.hero.type === 'shield' && ( + {slideDefinition.hero.type === "shield" && ( )} - {slideDefinition.hero.type === 'lock' && ( + {slideDefinition.hero.type === "lock" && ( )} - {slideDefinition.hero.type === 'analytics' && ( + {slideDefinition.hero.type === "analytics" && ( )} - {slideDefinition.hero.type === 'diamond' && } - {slideDefinition.hero.type === 'logo' && ( + {slideDefinition.hero.type === "diamond" && } + {slideDefinition.hero.type === "logo" && ( Stirling logo )}
@@ -88,8 +87,8 @@ export default function OnboardingModalSlide({ withCloseButton={false} zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE} styles={{ - body: { padding: 0, maxHeight: '90vh', overflow: 'hidden' }, - content: { overflow: 'hidden', border: 'none', background: 'var(--bg-surface)', maxHeight: '90vh' }, + body: { padding: 0, maxHeight: "90vh", overflow: "hidden" }, + content: { overflow: "hidden", border: "none", background: "var(--bg-surface)", maxHeight: "90vh" }, }} > @@ -106,18 +105,18 @@ export default function OnboardingModalSlide({ radius="md" size={36} style={{ - position: 'absolute', + position: "absolute", top: 16, right: 16, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - color: 'white', - backdropFilter: 'blur(4px)', + backgroundColor: "rgba(255, 255, 255, 0.2)", + color: "white", + backdropFilter: "blur(4px)", zIndex: 10, }} styles={{ root: { - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.3)', + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.3)", }, }, }} @@ -130,12 +129,9 @@ export default function OnboardingModalSlide({
-
+
-
+
{slideContent.title}
@@ -146,9 +142,7 @@ export default function OnboardingModalSlide({
- {modalSlideCount > 1 && ( - - )} + {modalSlideCount > 1 && }
); } - diff --git a/frontend/src/core/components/onboarding/OnboardingStepper.tsx b/frontend/src/core/components/onboarding/OnboardingStepper.tsx index ec6767d8ad..23c37643da 100644 --- a/frontend/src/core/components/onboarding/OnboardingStepper.tsx +++ b/frontend/src/core/components/onboarding/OnboardingStepper.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface OnboardingStepperProps { totalSteps: number; @@ -17,18 +17,16 @@ export function OnboardingStepper({ totalSteps, activeStep, className }: Onboard
{items.map((index) => { const isActive = index === activeStep; const baseStyles: React.CSSProperties = { - background: isActive - ? 'var(--onboarding-step-active)' - : 'var(--onboarding-step-inactive)', + background: isActive ? "var(--onboarding-step-active)" : "var(--onboarding-step-inactive)", }; return ( @@ -48,5 +46,3 @@ export function OnboardingStepper({ totalSteps, activeStep, className }: Onboard } export default OnboardingStepper; - - diff --git a/frontend/src/core/components/onboarding/OnboardingTour.css b/frontend/src/core/components/onboarding/OnboardingTour.css index 54ad69d68d..a1cbd4f3d8 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.css +++ b/frontend/src/core/components/onboarding/OnboardingTour.css @@ -18,7 +18,8 @@ } @keyframes pulse-glow { - 0%, 100% { + 0%, + 100% { box-shadow: 0 0 0 3px var(--mantine-primary-color-filled), 0 0 20px var(--mantine-primary-color-filled), @@ -33,13 +34,13 @@ } /* RTL: mirror step indicator and controls in Reactour popovers */ -:root[dir='rtl'] .reactour__popover { +:root[dir="rtl"] .reactour__popover { direction: rtl; } /* Minimal overrides retained for glow only */ -:root[dir='rtl'] .reactour__badge { +:root[dir="rtl"] .reactour__badge { left: auto; right: 16px; } diff --git a/frontend/src/core/components/onboarding/OnboardingTour.tsx b/frontend/src/core/components/onboarding/OnboardingTour.tsx index 85df0a9fdb..9ddb4e4c04 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.tsx +++ b/frontend/src/core/components/onboarding/OnboardingTour.tsx @@ -1,19 +1,19 @@ /** * OnboardingTour Component - * + * * Reusable tour wrapper that encapsulates all Reactour configuration. * Used by the main Onboarding component for both the 'tour' step and * when the tour is open but onboarding is inactive. */ -import React from 'react'; -import { TourProvider, useTour, type StepType } from '@reactour/tour'; -import { CloseButton, ActionIcon } from '@mantine/core'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import CheckIcon from '@mui/icons-material/Check'; -import type { TFunction } from 'i18next'; -import i18n from '@app/i18n'; +import React from "react"; +import { TourProvider, useTour, type StepType } from "@reactour/tour"; +import { CloseButton, ActionIcon } from "@mantine/core"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import CheckIcon from "@mui/icons-material/Check"; +import type { TFunction } from "i18next"; +import i18n from "@app/i18n"; /** * TourContent - Controls the tour visibility @@ -49,7 +49,7 @@ interface CloseArgs { interface OnboardingTourProps { tourSteps: StepType[]; - tourType: 'admin' | 'tools' | 'whatsnew'; + tourType: "admin" | "tools" | "whatsnew"; isRTL: boolean; t: TFunction; isOpen: boolean; @@ -57,22 +57,14 @@ interface OnboardingTourProps { onClose: (args: CloseArgs) => void; } -export default function OnboardingTour({ - tourSteps, - tourType, - isRTL, - t, - isOpen, - onAdvance, - onClose, -}: OnboardingTourProps) { +export default function OnboardingTour({ tourSteps, tourType, isRTL, t, isOpen, onAdvance, onClose }: OnboardingTourProps) { if (!isOpen) return null; return ( { @@ -80,10 +72,10 @@ export default function OnboardingTour({ onAdvance(clickProps); }} keyboardHandler={(e, clickProps, status) => { - if (e.key === 'ArrowRight' && !status?.isRightDisabled && clickProps) { + if (e.key === "ArrowRight" && !status?.isRightDisabled && clickProps) { e.preventDefault(); onAdvance(clickProps); - } else if (e.key === 'Escape' && !status?.isEscDisabled && clickProps) { + } else if (e.key === "Escape" && !status?.isEscDisabled && clickProps) { e.preventDefault(); onClose(clickProps); } @@ -92,12 +84,12 @@ export default function OnboardingTour({ styles={{ popover: (base) => ({ ...base, - backgroundColor: 'var(--mantine-color-body)', - color: 'var(--mantine-color-text)', - borderRadius: '8px', - padding: '20px', - boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', - maxWidth: '400px', + backgroundColor: "var(--mantine-color-body)", + color: "var(--mantine-color-text)", + borderRadius: "8px", + padding: "20px", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", + maxWidth: "400px", }), maskArea: (base) => ({ ...base, @@ -105,11 +97,11 @@ export default function OnboardingTour({ }), badge: (base) => ({ ...base, - backgroundColor: 'var(--mantine-primary-color-filled)', + backgroundColor: "var(--mantine-primary-color-filled)", }), controls: (base) => ({ ...base, - justifyContent: 'center', + justifyContent: "center", }), }} highlightedMaskClassName="tour-highlight-glow" @@ -127,7 +119,7 @@ export default function OnboardingTour({ onClick={() => onAdvance({ setCurrentStep, currentStep: tourCurrentStep, steps: tourSteps, setIsOpen })} variant="subtle" size="lg" - aria-label={isLast ? t('onboarding.finish', 'Finish') : t('onboarding.next', 'Next')} + aria-label={isLast ? t("onboarding.finish", "Finish") : t("onboarding.next", "Next")} > {isLast ? : } @@ -135,10 +127,10 @@ export default function OnboardingTour({ }} components={{ Close: ({ onClick }) => ( - + ), Content: ({ content }: { content: string }) => ( -
+
), }} > @@ -148,4 +140,3 @@ export default function OnboardingTour({ } export type { AdvanceArgs, CloseArgs }; - diff --git a/frontend/src/core/components/onboarding/adminStepsConfig.ts b/frontend/src/core/components/onboarding/adminStepsConfig.ts index b7a5c0b66c..7946c9fec7 100644 --- a/frontend/src/core/components/onboarding/adminStepsConfig.ts +++ b/frontend/src/core/components/onboarding/adminStepsConfig.ts @@ -1,6 +1,6 @@ -import type { StepType } from '@reactour/tour'; -import type { TFunction } from 'i18next'; -import { addGlowToElements, removeAllGlows } from '@app/components/onboarding/tourGlow'; +import type { StepType } from "@reactour/tour"; +import type { TFunction } from "i18next"; +import { addGlowToElements, removeAllGlows } from "@app/components/onboarding/tourGlow"; export enum AdminTourStep { WELCOME, @@ -14,7 +14,7 @@ export enum AdminTourStep { WRAP_UP, } -interface AdminStepActions { +interface AdminStepActions { saveAdminState: () => void; openConfigModal: () => void; navigateToSection: (section: string) => void; @@ -32,8 +32,11 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg return { [AdminTourStep.WELCOME]: { selector: '[data-tour="config-button"]', - content: t('adminOnboarding.welcome', "Welcome to the Admin Tour! Let's explore the powerful enterprise features and settings available to system administrators."), - position: 'right', + content: t( + "adminOnboarding.welcome", + "Welcome to the Admin Tour! Let's explore the powerful enterprise features and settings available to system administrators.", + ), + position: "right", padding: 10, action: () => { saveAdminState(); @@ -41,17 +44,23 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg }, [AdminTourStep.CONFIG_BUTTON]: { selector: '[data-tour="config-button"]', - content: t('adminOnboarding.configButton', "Click the Config button to access all system settings and administrative controls."), - position: 'right', + content: t( + "adminOnboarding.configButton", + "Click the Config button to access all system settings and administrative controls.", + ), + position: "right", padding: 10, actionAfter: () => { openConfigModal(); }, }, [AdminTourStep.SETTINGS_OVERVIEW]: { - selector: '.modal-nav', - content: t('adminOnboarding.settingsOverview', "This is the Settings Panel. Admin settings are organised by category for easy navigation."), - position: 'right', + selector: ".modal-nav", + content: t( + "adminOnboarding.settingsOverview", + "This is the Settings Panel. Admin settings are organised by category for easy navigation.", + ), + position: "right", padding: 0, action: () => { removeAllGlows(); @@ -59,41 +68,68 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg }, [AdminTourStep.TEAMS_AND_USERS]: { selector: '[data-tour="admin-people-nav"]', - highlightedSelectors: ['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.teamsAndUsers', "Manage Teams and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-people-nav"]', + '[data-tour="admin-teams-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.teamsAndUsers", + "Manage Teams and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('people'); + navigateToSection("people"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-people-nav"]', + '[data-tour="admin-teams-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.SYSTEM_CUSTOMIZATION]: { selector: '[data-tour="admin-adminGeneral-nav"]', - highlightedSelectors: ['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.systemCustomization', "We have extensive ways to customise the UI: System Settings let you change the app name and languages, Features allows server certificate management, and Endpoints lets you enable or disable specific tools for your users."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-adminGeneral-nav"]', + '[data-tour="admin-adminFeatures-nav"]', + '[data-tour="admin-adminEndpoints-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.systemCustomization", + "We have extensive ways to customise the UI: System Settings let you change the app name and languages, Features allows server certificate management, and Endpoints lets you enable or disable specific tools for your users.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminGeneral'); + navigateToSection("adminGeneral"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-adminGeneral-nav"]', + '[data-tour="admin-adminFeatures-nav"]', + '[data-tour="admin-adminEndpoints-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.DATABASE_SECTION]: { selector: '[data-tour="admin-adminDatabase-nav"]', highlightedSelectors: ['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.databaseSection', "For advanced production environments, we have settings to allow external database hookups so you can integrate with your existing infrastructure."), - position: 'right', + content: t( + "adminOnboarding.databaseSection", + "For advanced production environments, we have settings to allow external database hookups so you can integrate with your existing infrastructure.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminDatabase'); + navigateToSection("adminDatabase"); setTimeout(() => { addGlowToElements(['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]']); }, 100); @@ -102,38 +138,55 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg [AdminTourStep.CONNECTIONS_SECTION]: { selector: '[data-tour="admin-adminConnections-nav"]', highlightedSelectors: ['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.connectionsSection', "The Connections section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications."), - position: 'right', + content: t( + "adminOnboarding.connectionsSection", + "The Connections section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminConnections'); + navigateToSection("adminConnections"); setTimeout(() => { addGlowToElements(['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]']); }, 100); }, actionAfter: async () => { - await scrollNavToSection('adminAudit'); + await scrollNavToSection("adminAudit"); }, }, [AdminTourStep.ADMIN_TOOLS]: { selector: '[data-tour="admin-adminAudit-nav"]', - highlightedSelectors: ['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.adminTools', "Finally, we have advanced administration tools like Auditing to track system activity and Usage Analytics to monitor how your users interact with the platform."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-adminAudit-nav"]', + '[data-tour="admin-adminUsage-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.adminTools", + "Finally, we have advanced administration tools like Auditing to track system activity and Usage Analytics to monitor how your users interact with the platform.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminAudit'); + navigateToSection("adminAudit"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-adminAudit-nav"]', + '[data-tour="admin-adminUsage-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.WRAP_UP]: { selector: '[data-tour="help-button"]', - content: t('adminOnboarding.wrapUp', "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the Help menu."), - position: 'right', + content: t( + "adminOnboarding.wrapUp", + "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the Help menu.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); @@ -141,4 +194,3 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg }, }; } - diff --git a/frontend/src/core/components/onboarding/onboardingFlowConfig.ts b/frontend/src/core/components/onboarding/onboardingFlowConfig.ts index 101fb13eef..c569954cdb 100644 --- a/frontend/src/core/components/onboarding/onboardingFlowConfig.ts +++ b/frontend/src/core/components/onboarding/onboardingFlowConfig.ts @@ -1,45 +1,45 @@ -import WelcomeSlide from '@app/components/onboarding/slides/WelcomeSlide'; -import DesktopInstallSlide from '@app/components/onboarding/slides/DesktopInstallSlide'; -import SecurityCheckSlide from '@app/components/onboarding/slides/SecurityCheckSlide'; -import PlanOverviewSlide from '@app/components/onboarding/slides/PlanOverviewSlide'; -import ServerLicenseSlide from '@app/components/onboarding/slides/ServerLicenseSlide'; -import FirstLoginSlide from '@app/components/onboarding/slides/FirstLoginSlide'; -import TourOverviewSlide from '@app/components/onboarding/slides/TourOverviewSlide'; -import AnalyticsChoiceSlide from '@app/components/onboarding/slides/AnalyticsChoiceSlide'; -import MFASetupSlide from '@app/components/onboarding/slides/MFASetupSlide'; -import { SlideConfig, LicenseNotice } from '@app/types/types'; +import WelcomeSlide from "@app/components/onboarding/slides/WelcomeSlide"; +import DesktopInstallSlide from "@app/components/onboarding/slides/DesktopInstallSlide"; +import SecurityCheckSlide from "@app/components/onboarding/slides/SecurityCheckSlide"; +import PlanOverviewSlide from "@app/components/onboarding/slides/PlanOverviewSlide"; +import ServerLicenseSlide from "@app/components/onboarding/slides/ServerLicenseSlide"; +import FirstLoginSlide from "@app/components/onboarding/slides/FirstLoginSlide"; +import TourOverviewSlide from "@app/components/onboarding/slides/TourOverviewSlide"; +import AnalyticsChoiceSlide from "@app/components/onboarding/slides/AnalyticsChoiceSlide"; +import MFASetupSlide from "@app/components/onboarding/slides/MFASetupSlide"; +import { SlideConfig, LicenseNotice } from "@app/types/types"; export type SlideId = - | 'first-login' - | 'welcome' - | 'desktop-install' - | 'security-check' - | 'admin-overview' - | 'server-license' - | 'tour-overview' - | 'analytics-choice' - | 'mfa-setup'; + | "first-login" + | "welcome" + | "desktop-install" + | "security-check" + | "admin-overview" + | "server-license" + | "tour-overview" + | "analytics-choice" + | "mfa-setup"; -export type HeroType = 'rocket' | 'dual-icon' | 'shield' | 'diamond' | 'logo' | 'lock' | 'analytics'; +export type HeroType = "rocket" | "dual-icon" | "shield" | "diamond" | "logo" | "lock" | "analytics"; export type ButtonAction = - | 'next' - | 'prev' - | 'close' - | 'complete-close' - | 'download-selected' - | 'security-next' - | 'launch-admin' - | 'launch-tools' - | 'launch-auto' - | 'see-plans' - | 'skip-to-license' - | 'skip-tour' - | 'enable-analytics' - | 'disable-analytics'; + | "next" + | "prev" + | "close" + | "complete-close" + | "download-selected" + | "security-next" + | "launch-admin" + | "launch-tools" + | "launch-auto" + | "see-plans" + | "skip-to-license" + | "skip-tour" + | "enable-analytics" + | "disable-analytics"; export interface FlowState { - selectedRole: 'admin' | 'user' | null; + selectedRole: "admin" | "user" | null; } export interface OSOption { @@ -53,8 +53,8 @@ export interface SlideFactoryParams { osUrl: string; osOptions?: OSOption[]; onDownloadUrlChange?: (url: string) => void; - selectedRole: 'admin' | 'user' | null; - onRoleSelect: (role: 'admin' | 'user' | null) => void; + selectedRole: "admin" | "user" | null; + onRoleSelect: (role: "admin" | "user" | null) => void; licenseNotice?: LicenseNotice; loginEnabled?: boolean; // First login params @@ -72,11 +72,11 @@ export interface HeroDefinition { export interface ButtonDefinition { key: string; - type: 'button' | 'icon'; + type: "button" | "icon"; label?: string; - icon?: 'chevron-left'; - variant?: 'primary' | 'secondary' | 'default'; - group: 'left' | 'right'; + icon?: "chevron-left"; + variant?: "primary" | "secondary" | "default"; + group: "left" | "right"; action: ButtonAction; disabledWhen?: (state: FlowState) => boolean; } @@ -89,206 +89,204 @@ export interface SlideDefinition { } export const SLIDE_DEFINITIONS: Record = { - 'first-login': { - id: 'first-login', + "first-login": { + id: "first-login", createSlide: ({ firstLoginUsername, onPasswordChanged, usingDefaultCredentials }) => FirstLoginSlide({ - username: firstLoginUsername || '', + username: firstLoginUsername || "", onPasswordChanged: onPasswordChanged || (() => {}), usingDefaultCredentials: usingDefaultCredentials || false, }), - hero: { type: 'lock' }, + hero: { type: "lock" }, buttons: [], // Form has its own submit button }, - 'welcome': { - id: 'welcome', + welcome: { + id: "welcome", createSlide: () => WelcomeSlide(), - hero: { type: 'rocket' }, + hero: { type: "rocket" }, buttons: [ { - key: 'welcome-next', - type: 'button', - label: 'onboarding.buttons.next', - variant: 'primary', - group: 'right', - action: 'next', + key: "welcome-next", + type: "button", + label: "onboarding.buttons.next", + variant: "primary", + group: "right", + action: "next", }, ], }, - 'desktop-install': { - id: 'desktop-install', - createSlide: ({ osLabel, osUrl, osOptions, onDownloadUrlChange }) => DesktopInstallSlide({ osLabel, osUrl, osOptions, onDownloadUrlChange }), - hero: { type: 'dual-icon' }, + "desktop-install": { + id: "desktop-install", + createSlide: ({ osLabel, osUrl, osOptions, onDownloadUrlChange }) => + DesktopInstallSlide({ osLabel, osUrl, osOptions, onDownloadUrlChange }), + hero: { type: "dual-icon" }, buttons: [ { - key: 'desktop-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "desktop-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'desktop-skip', - type: 'button', - label: 'onboarding.buttons.skipForNow', - variant: 'secondary', - group: 'left', - action: 'next', + key: "desktop-skip", + type: "button", + label: "onboarding.buttons.skipForNow", + variant: "secondary", + group: "left", + action: "next", }, { - key: 'desktop-download', - type: 'button', - label: 'onboarding.buttons.download', - variant: 'primary', - group: 'right', - action: 'download-selected', + key: "desktop-download", + type: "button", + label: "onboarding.buttons.download", + variant: "primary", + group: "right", + action: "download-selected", }, ], }, - 'security-check': { - id: 'security-check', - createSlide: ({ selectedRole, onRoleSelect }) => - SecurityCheckSlide({ selectedRole, onRoleSelect }), - hero: { type: 'shield' }, + "security-check": { + id: "security-check", + createSlide: ({ selectedRole, onRoleSelect }) => SecurityCheckSlide({ selectedRole, onRoleSelect }), + hero: { type: "shield" }, buttons: [ { - key: 'security-back', - type: 'button', - label: 'onboarding.buttons.back', - variant: 'secondary', - group: 'left', - action: 'prev', + key: "security-back", + type: "button", + label: "onboarding.buttons.back", + variant: "secondary", + group: "left", + action: "prev", }, { - key: 'security-next', - type: 'button', - label: 'onboarding.buttons.next', - variant: 'primary', - group: 'right', - action: 'security-next', + key: "security-next", + type: "button", + label: "onboarding.buttons.next", + variant: "primary", + group: "right", + action: "security-next", disabledWhen: (state) => !state.selectedRole, }, ], }, - 'admin-overview': { - id: 'admin-overview', - createSlide: ({ licenseNotice, loginEnabled }) => - PlanOverviewSlide({ isAdmin: true, licenseNotice, loginEnabled }), - hero: { type: 'diamond' }, + "admin-overview": { + id: "admin-overview", + createSlide: ({ licenseNotice, loginEnabled }) => PlanOverviewSlide({ isAdmin: true, licenseNotice, loginEnabled }), + hero: { type: "diamond" }, buttons: [ { - key: 'admin-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "admin-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'admin-show', - type: 'button', - label: 'onboarding.buttons.showMeAround', - variant: 'primary', - group: 'right', - action: 'launch-admin', + key: "admin-show", + type: "button", + label: "onboarding.buttons.showMeAround", + variant: "primary", + group: "right", + action: "launch-admin", }, { - key: 'admin-skip', - type: 'button', - label: 'onboarding.buttons.skipTheTour', - variant: 'secondary', - group: 'left', - action: 'skip-to-license', + key: "admin-skip", + type: "button", + label: "onboarding.buttons.skipTheTour", + variant: "secondary", + group: "left", + action: "skip-to-license", }, ], }, - 'server-license': { - id: 'server-license', + "server-license": { + id: "server-license", createSlide: ({ licenseNotice }) => ServerLicenseSlide({ licenseNotice }), - hero: { type: 'dual-icon' }, + hero: { type: "dual-icon" }, buttons: [ { - key: 'license-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "license-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'license-close', - type: 'button', - label: 'onboarding.buttons.skipForNow', - variant: 'secondary', - group: 'left', - action: 'close', + key: "license-close", + type: "button", + label: "onboarding.buttons.skipForNow", + variant: "secondary", + group: "left", + action: "close", }, { - key: 'license-see-plans', - type: 'button', - label: 'onboarding.serverLicense.seePlans', - variant: 'primary', - group: 'right', - action: 'see-plans', + key: "license-see-plans", + type: "button", + label: "onboarding.serverLicense.seePlans", + variant: "primary", + group: "right", + action: "see-plans", }, ], }, - 'tour-overview': { - id: 'tour-overview', + "tour-overview": { + id: "tour-overview", createSlide: () => TourOverviewSlide(), - hero: { type: 'rocket' }, + hero: { type: "rocket" }, buttons: [ { - key: 'tour-overview-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "tour-overview-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'tour-overview-skip', - type: 'button', - label: 'onboarding.buttons.skipForNow', - variant: 'secondary', - group: 'left', - action: 'skip-tour', + key: "tour-overview-skip", + type: "button", + label: "onboarding.buttons.skipForNow", + variant: "secondary", + group: "left", + action: "skip-tour", }, { - key: 'tour-overview-show', - type: 'button', - label: 'onboarding.buttons.showMeAround', - variant: 'primary', - group: 'right', - action: 'launch-tools', + key: "tour-overview-show", + type: "button", + label: "onboarding.buttons.showMeAround", + variant: "primary", + group: "right", + action: "launch-tools", }, ], }, - 'analytics-choice': { - id: 'analytics-choice', + "analytics-choice": { + id: "analytics-choice", createSlide: ({ analyticsError }) => AnalyticsChoiceSlide({ analyticsError }), - hero: { type: 'analytics' }, + hero: { type: "analytics" }, buttons: [ { - key: 'analytics-disable', - type: 'button', - label: 'no', - variant: 'secondary', - group: 'left', - action: 'disable-analytics', + key: "analytics-disable", + type: "button", + label: "no", + variant: "secondary", + group: "left", + action: "disable-analytics", }, { - key: 'analytics-enable', - type: 'button', - label: 'yes', - variant: 'primary', - group: 'right', - action: 'enable-analytics', + key: "analytics-enable", + type: "button", + label: "yes", + variant: "primary", + group: "right", + action: "enable-analytics", }, ], }, - 'mfa-setup': { - id: 'mfa-setup', + "mfa-setup": { + id: "mfa-setup", createSlide: ({ onMfaSetupComplete = () => {} }: SlideFactoryParams) => MFASetupSlide({ onMfaSetupComplete }), - hero: { type: 'lock' }, + hero: { type: "lock" }, buttons: [], // Form has its own submit button }, }; - diff --git a/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts b/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts index 9528694c3e..a4f06e4c72 100644 --- a/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts +++ b/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts @@ -1,23 +1,21 @@ export type OnboardingStepId = - | 'first-login' - | 'welcome' - | 'desktop-install' - | 'security-check' - | 'admin-overview' - | 'tool-layout' - | 'tour-overview' - | 'server-license' - | 'analytics-choice' - | 'mfa-setup'; + | "first-login" + | "welcome" + | "desktop-install" + | "security-check" + | "admin-overview" + | "tool-layout" + | "tour-overview" + | "server-license" + | "analytics-choice" + | "mfa-setup"; -export type OnboardingStepType = - | 'modal-slide' - | 'tool-prompt'; +export type OnboardingStepType = "modal-slide" | "tool-prompt"; export interface OnboardingRuntimeState { - selectedRole: 'admin' | 'user' | null; + selectedRole: "admin" | "user" | null; tourRequested: boolean; - tourType: 'admin' | 'tools' | 'whatsnew'; + tourType: "admin" | "tools" | "whatsnew"; isDesktopApp: boolean; desktopSlideEnabled: boolean; analyticsNotConfigured: boolean; @@ -43,14 +41,23 @@ export interface OnboardingStep { id: OnboardingStepId; type: OnboardingStepType; condition: (ctx: OnboardingConditionContext) => boolean; - slideId?: 'first-login' | 'welcome' | 'desktop-install' | 'security-check' | 'admin-overview' | 'server-license' | 'tour-overview' | 'analytics-choice' | 'mfa-setup'; + slideId?: + | "first-login" + | "welcome" + | "desktop-install" + | "security-check" + | "admin-overview" + | "server-license" + | "tour-overview" + | "analytics-choice" + | "mfa-setup"; allowDismiss?: boolean; } export const DEFAULT_RUNTIME_STATE: OnboardingRuntimeState = { selectedRole: null, tourRequested: false, - tourType: 'whatsnew', + tourType: "whatsnew", isDesktopApp: false, analyticsNotConfigured: false, analyticsEnabled: false, @@ -61,7 +68,7 @@ export const DEFAULT_RUNTIME_STATE: OnboardingRuntimeState = { requiresLicense: false, }, requiresPasswordChange: false, - firstLoginUsername: '', + firstLoginUsername: "", usingDefaultCredentials: false, desktopSlideEnabled: true, requiresMfaSetup: false, @@ -69,59 +76,59 @@ export const DEFAULT_RUNTIME_STATE: OnboardingRuntimeState = { export const ONBOARDING_STEPS: OnboardingStep[] = [ { - id: 'first-login', - type: 'modal-slide', - slideId: 'first-login', + id: "first-login", + type: "modal-slide", + slideId: "first-login", condition: (ctx) => ctx.requiresPasswordChange, }, { - id: 'welcome', - type: 'modal-slide', - slideId: 'welcome', + id: "welcome", + type: "modal-slide", + slideId: "welcome", // Desktop has its own onboarding modal (DesktopOnboardingModal) condition: (ctx) => !ctx.isDesktopApp, }, { - id: 'admin-overview', - type: 'modal-slide', - slideId: 'admin-overview', + id: "admin-overview", + type: "modal-slide", + slideId: "admin-overview", condition: (ctx) => ctx.effectiveIsAdmin, }, { - id: 'desktop-install', - type: 'modal-slide', - slideId: 'desktop-install', + id: "desktop-install", + type: "modal-slide", + slideId: "desktop-install", condition: (ctx) => !ctx.isDesktopApp && ctx.desktopSlideEnabled, }, { - id: 'security-check', - type: 'modal-slide', - slideId: 'security-check', + id: "security-check", + type: "modal-slide", + slideId: "security-check", condition: () => false, }, { - id: 'tool-layout', - type: 'tool-prompt', + id: "tool-layout", + type: "tool-prompt", condition: () => false, }, { - id: 'tour-overview', - type: 'modal-slide', - slideId: 'tour-overview', - condition: (ctx) => !ctx.effectiveIsAdmin && ctx.tourType !== 'admin' && !ctx.isDesktopApp, + id: "tour-overview", + type: "modal-slide", + slideId: "tour-overview", + condition: (ctx) => !ctx.effectiveIsAdmin && ctx.tourType !== "admin" && !ctx.isDesktopApp, }, { - id: 'server-license', - type: 'modal-slide', - slideId: 'server-license', + id: "server-license", + type: "modal-slide", + slideId: "server-license", condition: (ctx) => ctx.effectiveIsAdmin && ctx.licenseNotice.requiresLicense, }, { - id: 'mfa-setup', - type: 'modal-slide', - slideId: 'mfa-setup', + id: "mfa-setup", + type: "modal-slide", + slideId: "mfa-setup", condition: (ctx) => ctx.requiresMfaSetup, - } + }, ]; export function getStepById(id: OnboardingStepId): OnboardingStep | undefined { @@ -131,4 +138,3 @@ export function getStepById(id: OnboardingStepId): OnboardingStep | undefined { export function getStepIndex(id: OnboardingStepId): number { return ONBOARDING_STEPS.findIndex((step) => step.id === id); } - diff --git a/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts b/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts index 9e3065614a..08a8049a1b 100644 --- a/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts +++ b/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts @@ -1,62 +1,62 @@ -const STORAGE_PREFIX = 'onboarding'; +const STORAGE_PREFIX = "onboarding"; const TOURS_TOOLTIP_KEY = `${STORAGE_PREFIX}::tours-tooltip-shown`; const ONBOARDING_COMPLETED_KEY = `${STORAGE_PREFIX}::completed`; export function isOnboardingCompleted(): boolean { - if (typeof window === 'undefined') return false; + if (typeof window === "undefined") return false; try { - return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true'; + return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === "true"; } catch { return false; } } export function markOnboardingCompleted(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { - localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true'); + localStorage.setItem(ONBOARDING_COMPLETED_KEY, "true"); } catch (error) { - console.error('[onboardingStorage] Error marking onboarding as completed:', error); + console.error("[onboardingStorage] Error marking onboarding as completed:", error); } } export function resetOnboardingProgress(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { localStorage.removeItem(ONBOARDING_COMPLETED_KEY); } catch (error) { - console.error('[onboardingStorage] Error resetting onboarding progress:', error); + console.error("[onboardingStorage] Error resetting onboarding progress:", error); } } export function hasShownToursTooltip(): boolean { - if (typeof window === 'undefined') return false; + if (typeof window === "undefined") return false; try { - return localStorage.getItem(TOURS_TOOLTIP_KEY) === 'true'; + return localStorage.getItem(TOURS_TOOLTIP_KEY) === "true"; } catch { return false; } } export function markToursTooltipShown(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { - localStorage.setItem(TOURS_TOOLTIP_KEY, 'true'); + localStorage.setItem(TOURS_TOOLTIP_KEY, "true"); } catch (error) { - console.error('[onboardingStorage] Error marking tours tooltip as shown:', error); + console.error("[onboardingStorage] Error marking tours tooltip as shown:", error); } } export function migrateFromLegacyPreferences(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; const migrationKey = `${STORAGE_PREFIX}::migrated`; try { // Skip if already migrated - if (localStorage.getItem(migrationKey) === 'true') return; + if (localStorage.getItem(migrationKey) === "true") return; - const prefsRaw = localStorage.getItem('stirlingpdf_preferences'); + const prefsRaw = localStorage.getItem("stirlingpdf_preferences"); if (prefsRaw) { const prefs = JSON.parse(prefsRaw) as Record; @@ -67,7 +67,7 @@ export function migrateFromLegacyPreferences(): void { } // Mark migration complete - localStorage.setItem(migrationKey, 'true'); + localStorage.setItem(migrationKey, "true"); } catch { // If migration fails, onboarding will show again - safer than hiding it } diff --git a/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts b/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts index 54d1d2acf4..4fade3a3fc 100644 --- a/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts +++ b/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts @@ -1,7 +1,7 @@ -import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; -import { useLocation } from 'react-router-dom'; -import { useServerExperience } from '@app/hooks/useServerExperience'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; +import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; +import { useServerExperience } from "@app/hooks/useServerExperience"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; import { ONBOARDING_STEPS, @@ -10,39 +10,40 @@ import { type OnboardingRuntimeState, type OnboardingConditionContext, DEFAULT_RUNTIME_STATE, -} from '@app/components/onboarding/orchestrator/onboardingConfig'; +} from "@app/components/onboarding/orchestrator/onboardingConfig"; import { isOnboardingCompleted, markOnboardingCompleted, migrateFromLegacyPreferences, -} from '@app/components/onboarding/orchestrator/onboardingStorage'; -import { accountService } from '@app/services/accountService'; -import { useBypassOnboarding } from '@app/components/onboarding/useBypassOnboarding'; +} from "@app/components/onboarding/orchestrator/onboardingStorage"; +import { accountService } from "@app/services/accountService"; +import { useBypassOnboarding } from "@app/components/onboarding/useBypassOnboarding"; -const AUTH_ROUTES = ['/login', '/signup', '/auth', '/invite']; -const SESSION_TOUR_REQUESTED = 'onboarding::session::tour-requested'; -const SESSION_TOUR_TYPE = 'onboarding::session::tour-type'; -const SESSION_SELECTED_ROLE = 'onboarding::session::selected-role'; +const AUTH_ROUTES = ["/login", "/signup", "/auth", "/invite"]; +const SESSION_TOUR_REQUESTED = "onboarding::session::tour-requested"; +const SESSION_TOUR_TYPE = "onboarding::session::tour-type"; +const SESSION_SELECTED_ROLE = "onboarding::session::selected-role"; // Check if user has an auth token (to avoid flash before redirect) function hasAuthToken(): boolean { - if (typeof window === 'undefined') return false; - return !!localStorage.getItem('stirling_jwt'); + if (typeof window === "undefined") return false; + return !!localStorage.getItem("stirling_jwt"); } // Get initial runtime state from session storage (survives remounts) function getInitialRuntimeState(baseState: OnboardingRuntimeState): OnboardingRuntimeState { - if (typeof window === 'undefined') { + if (typeof window === "undefined") { return baseState; } try { - const tourRequested = sessionStorage.getItem(SESSION_TOUR_REQUESTED) === 'true'; + const tourRequested = sessionStorage.getItem(SESSION_TOUR_REQUESTED) === "true"; const sessionTourType = sessionStorage.getItem(SESSION_TOUR_TYPE); - const tourType = (sessionTourType === 'admin' || sessionTourType === 'tools' || sessionTourType === 'whatsnew') - ? sessionTourType - : 'whatsnew'; - const selectedRole = sessionStorage.getItem(SESSION_SELECTED_ROLE) as 'admin' | 'user' | null; + const tourType = + sessionTourType === "admin" || sessionTourType === "tools" || sessionTourType === "whatsnew" + ? sessionTourType + : "whatsnew"; + const selectedRole = sessionStorage.getItem(SESSION_SELECTED_ROLE) as "admin" | "user" | null; return { ...baseState, @@ -56,11 +57,11 @@ function getInitialRuntimeState(baseState: OnboardingRuntimeState): OnboardingRu } function persistRuntimeState(state: Partial): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { if (state.tourRequested !== undefined) { - sessionStorage.setItem(SESSION_TOUR_REQUESTED, state.tourRequested ? 'true' : 'false'); + sessionStorage.setItem(SESSION_TOUR_REQUESTED, state.tourRequested ? "true" : "false"); } if (state.tourType !== undefined) { sessionStorage.setItem(SESSION_TOUR_TYPE, state.tourType); @@ -73,12 +74,12 @@ function persistRuntimeState(state: Partial): void { } } } catch (error) { - console.error('[useOnboardingOrchestrator] Error persisting runtime state:', error); + console.error("[useOnboardingOrchestrator] Error persisting runtime state:", error); } } function clearRuntimeStateSession(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { sessionStorage.removeItem(SESSION_TOUR_REQUESTED); @@ -94,9 +95,9 @@ function parseMfaRequired(settings: string | null | undefined): boolean { try { const parsed = JSON.parse(settings) as { mfaRequired?: string }; - return parsed.mfaRequired?.toLowerCase() === 'true'; + return parsed.mfaRequired?.toLowerCase() === "true"; } catch (error) { - console.warn('[useOnboardingOrchestrator] Failed to parse account settings JSON:', error); + console.warn("[useOnboardingOrchestrator] Failed to parse account settings JSON:", error); return false; } } @@ -151,18 +152,14 @@ export interface UseOnboardingOrchestratorOptions { defaultRuntimeState?: OnboardingRuntimeState; } -export function useOnboardingOrchestrator( - options?: UseOnboardingOrchestratorOptions -): UseOnboardingOrchestratorResult { +export function useOnboardingOrchestrator(options?: UseOnboardingOrchestratorOptions): UseOnboardingOrchestratorResult { const defaultState = options?.defaultRuntimeState ?? DEFAULT_RUNTIME_STATE; const serverExperience = useServerExperience(); const { config, loading: configLoading } = useAppConfig(); const location = useLocation(); const bypassOnboarding = useBypassOnboarding(); - const [runtimeState, setRuntimeState] = useState(() => - getInitialRuntimeState(defaultState) - ); + const [runtimeState, setRuntimeState] = useState(() => getInitialRuntimeState(defaultState)); const [isPaused, setIsPaused] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [currentStepIndex, setCurrentStepIndex] = useState(-1); @@ -186,10 +183,10 @@ export function useOnboardingOrchestrator( totalUsers: serverExperience.totalUsers, freeTierLimit: serverExperience.freeTierLimit, isOverLimit: serverExperience.overFreeTierLimit ?? false, - requiresLicense: !serverExperience.hasPaidLicense && ( - serverExperience.overFreeTierLimit === true || - (serverExperience.effectiveIsAdmin && serverExperience.userCountResolved) - ), + requiresLicense: + !serverExperience.hasPaidLicense && + (serverExperience.overFreeTierLimit === true || + (serverExperience.effectiveIsAdmin && serverExperience.userCountResolved)), }, })); }, [ @@ -220,7 +217,7 @@ export function useOnboardingOrchestrator( requiresMfaSetup: parseMfaRequired(accountData.settings), })); } catch (error) { - console.log('[OnboardingOrchestrator] Failed to fetch account data for onboarding runtime state:', error); + console.log("[OnboardingOrchestrator] Failed to fetch account data for onboarding runtime state:", error); // Account endpoint failed - user not logged in or security disabled } }; @@ -233,26 +230,25 @@ export function useOnboardingOrchestrator( const isOnAuthRoute = AUTH_ROUTES.some((route) => location.pathname.startsWith(route)); const loginEnabled = config?.enableLogin === true; const isUnauthenticatedWithLoginEnabled = loginEnabled && !hasAuthToken(); - const shouldBlockOnboarding = - bypassOnboarding || isOnAuthRoute || configLoading || isUnauthenticatedWithLoginEnabled; + const shouldBlockOnboarding = bypassOnboarding || isOnAuthRoute || configLoading || isUnauthenticatedWithLoginEnabled; - const conditionContext = useMemo(() => ({ - ...serverExperience, - ...runtimeState, - effectiveIsAdmin: serverExperience.effectiveIsAdmin || - (!serverExperience.loginEnabled && runtimeState.selectedRole === 'admin'), - }), [serverExperience, runtimeState]); + const conditionContext = useMemo( + () => ({ + ...serverExperience, + ...runtimeState, + effectiveIsAdmin: + serverExperience.effectiveIsAdmin || (!serverExperience.loginEnabled && runtimeState.selectedRole === "admin"), + }), + [serverExperience, runtimeState], + ); const activeFlow = useMemo(() => { return ONBOARDING_STEPS.filter((step) => step.condition(conditionContext)); }, [conditionContext]); // Wait for config AND admin status before calculating initial step - const adminStatusResolved = !configLoading && ( - config?.enableLogin === false || - config?.enableLogin === undefined || - config?.isAdmin !== undefined - ); + const adminStatusResolved = + !configLoading && (config?.enableLogin === false || config?.enableLogin === undefined || config?.isAdmin !== undefined); useEffect(() => { if (configLoading || !adminStatusResolved) return; @@ -280,14 +276,15 @@ export function useOnboardingOrchestrator( const totalSteps = activeFlow.length; - const isComplete = isInitialized && - (totalSteps === 0 || currentStepIndex >= totalSteps || isOnboardingCompleted()); - const currentStep = (currentStepIndex >= 0 && currentStepIndex < totalSteps) - ? activeFlow[currentStepIndex] - : null; + const isComplete = isInitialized && (totalSteps === 0 || currentStepIndex >= totalSteps || isOnboardingCompleted()); + const currentStep = currentStepIndex >= 0 && currentStepIndex < totalSteps ? activeFlow[currentStepIndex] : null; const isActive = !shouldBlockOnboarding && !isPaused && !isComplete && isInitialized && currentStep !== null; - const isLoading = configLoading || !adminStatusResolved || !isInitialized || - !initialIndexSet.current || (currentStepIndex === -1 && activeFlow.length > 0); + const isLoading = + configLoading || + !adminStatusResolved || + !isInitialized || + !initialIndexSet.current || + (currentStepIndex === -1 && activeFlow.length > 0); useEffect(() => { if (!configLoading && !isInitialized) setIsInitialized(true); @@ -325,7 +322,6 @@ export function useOnboardingOrchestrator( setCurrentStepIndex(nextIndex); }, [currentStepIndex, totalSteps]); - const updateRuntimeState = useCallback((updates: Partial) => { persistRuntimeState(updates); setRuntimeState((prev) => ({ ...prev, ...updates })); @@ -336,13 +332,16 @@ export function useOnboardingOrchestrator( setCurrentStepIndex(-1); }, []); - const startStep = useCallback((stepId: OnboardingStepId) => { - const index = activeFlow.findIndex((step) => step.id === stepId); - if (index !== -1) { - setCurrentStepIndex(index); - setIsPaused(false); - } - }, [activeFlow]); + const startStep = useCallback( + (stepId: OnboardingStepId) => { + const index = activeFlow.findIndex((step) => step.id === stepId); + if (index !== -1) { + setCurrentStepIndex(index); + setIsPaused(false); + } + }, + [activeFlow], + ); const pause = useCallback(() => setIsPaused(true), []); const resume = useCallback(() => setIsPaused(false), []); diff --git a/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx b/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx index c04009ca4c..9521933cb3 100644 --- a/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import { Trans } from 'react-i18next'; -import { Button } from '@mantine/core'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import i18n from '@app/i18n'; -import { SlideConfig } from '@app/types/types'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css'; +import React from "react"; +import { Trans } from "react-i18next"; +import { Button } from "@mantine/core"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import i18n from "@app/i18n"; +import { SlideConfig } from "@app/types/types"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css"; interface AnalyticsChoiceSlideProps { analyticsError?: string | null; @@ -13,8 +13,8 @@ interface AnalyticsChoiceSlideProps { export default function AnalyticsChoiceSlide({ analyticsError }: AnalyticsChoiceSlideProps): SlideConfig { return { - key: 'analytics-choice', - title: i18n.t('analytics.title', 'Do you want to help make Stirling PDF better?'), + key: "analytics-choice", + title: i18n.t("analytics.title", "Do you want to help make Stirling PDF better?"), body: (
}} />
-
+
- {analyticsError && ( -
- {analyticsError} -
- )} + {analyticsError &&
{analyticsError}
}
), background: { - gradientStops: ['#0EA5E9', '#6366F1'], + gradientStops: ["#0EA5E9", "#6366F1"], circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx b/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx index 289b07b6c4..11bd671c58 100644 --- a/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx +++ b/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import styles from '@app/components/onboarding/slides/AnimatedSlideBackground.module.css'; -import { AnimatedSlideBackgroundProps } from '@app/types/types'; +import React from "react"; +import styles from "@app/components/onboarding/slides/AnimatedSlideBackground.module.css"; +import { AnimatedSlideBackgroundProps } from "@app/types/types"; type CircleStyles = React.CSSProperties & { - '--circle-move-x'?: string; - '--circle-move-y'?: string; - '--circle-duration'?: string; - '--circle-delay'?: string; + "--circle-move-x"?: string; + "--circle-move-y"?: string; + "--circle-duration"?: string; + "--circle-delay"?: string; }; interface AnimatedSlideBackgroundComponentProps extends AnimatedSlideBackgroundProps { @@ -14,11 +14,7 @@ interface AnimatedSlideBackgroundComponentProps extends AnimatedSlideBackgroundP slideKey: string; } -export default function AnimatedSlideBackground({ - gradientStops, - circles, - isActive, -}: AnimatedSlideBackgroundComponentProps) { +export default function AnimatedSlideBackground({ gradientStops, circles, isActive }: AnimatedSlideBackgroundComponentProps) { const [prevGradient, setPrevGradient] = React.useState<[string, string] | null>(null); const [currentGradient, setCurrentGradient] = React.useState<[string, string]>(gradientStops); const [isTransitioning, setIsTransitioning] = React.useState(false); @@ -31,13 +27,13 @@ export default function AnimatedSlideBackground({ setCurrentGradient(gradientStops); return; } - + // Only transition if gradient actually changed if (currentGradient[0] !== gradientStops[0] || currentGradient[1] !== gradientStops[1]) { // Store previous gradient and start transition setPrevGradient(currentGradient); setIsTransitioning(true); - + // Update to new gradient (will fade in) setCurrentGradient(gradientStops); } @@ -59,8 +55,8 @@ export default function AnimatedSlideBackground({ return (
{prevGradientStyle && isTransitioning && ( -
{ setPrevGradient(null); @@ -69,14 +65,14 @@ export default function AnimatedSlideBackground({ /> )}
{circles.map((circle, index) => { const { position, size, color, opacity, blur, amplitude = 48, duration = 15, delay = 0 } = circle; - const moveX = position === 'bottom-left' ? amplitude : -amplitude; - const moveY = position === 'bottom-left' ? -amplitude * 0.6 : amplitude * 0.6; + const moveX = position === "bottom-left" ? amplitude : -amplitude; + const moveY = position === "bottom-left" ? -amplitude * 0.6 : amplitude * 0.6; const circleStyle: CircleStyles = { width: size, @@ -84,17 +80,17 @@ export default function AnimatedSlideBackground({ background: color, opacity: opacity ?? 0.9, filter: blur ? `blur(${blur}px)` : undefined, - '--circle-move-x': `${moveX}px`, - '--circle-move-y': `${moveY}px`, - '--circle-duration': `${duration}s`, - '--circle-delay': `${delay}s`, + "--circle-move-x": `${moveX}px`, + "--circle-move-y": `${moveY}px`, + "--circle-duration": `${duration}s`, + "--circle-delay": `${delay}s`, }; const defaultOffset = -size / 2; const offsetX = circle.offsetX ?? 0; const offsetY = circle.offsetY ?? 0; - if (position === 'bottom-left') { + if (position === "bottom-left") { circleStyle.left = `${defaultOffset + offsetX}px`; circleStyle.bottom = `${defaultOffset + offsetY}px`; } else { @@ -102,13 +98,7 @@ export default function AnimatedSlideBackground({ circleStyle.top = `${defaultOffset + offsetY}px`; } - return ( -
- ); + return
; })}
); diff --git a/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx b/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx index 9cebfbb9cd..0ad0eebc29 100644 --- a/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SlideConfig } from '@app/types/types'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import { DesktopInstallTitle, type OSOption } from '@app/components/onboarding/slides/DesktopInstallTitle'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { SlideConfig } from "@app/types/types"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import { DesktopInstallTitle, type OSOption } from "@app/components/onboarding/slides/DesktopInstallTitle"; export type { OSOption }; @@ -19,8 +19,8 @@ const DesktopInstallBody = () => { return ( {t( - 'onboarding.desktopInstall.body', - 'Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer.', + "onboarding.desktopInstall.body", + "Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer.", )} ); @@ -32,11 +32,10 @@ export default function DesktopInstallSlide({ osOptions = [], onDownloadUrlChange, }: DesktopInstallSlideProps): SlideConfig { - return { - key: 'desktop-install', + key: "desktop-install", title: ( - , downloadUrl: osUrl, background: { - gradientStops: ['#2563EB', '#0EA5E9'], + gradientStops: ["#2563EB", "#0EA5E9"], circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx b/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx index ac42b518b3..b69b1b6b84 100644 --- a/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx +++ b/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Menu, ActionIcon } from '@mantine/core'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Menu, ActionIcon } from "@mantine/core"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; export interface OSOption { label: string; @@ -16,11 +16,11 @@ interface DesktopInstallTitleProps { onDownloadUrlChange?: (url: string) => void; } -export const DesktopInstallTitle: React.FC = ({ - osLabel, - osUrl, - osOptions, - onDownloadUrlChange +export const DesktopInstallTitle: React.FC = ({ + osLabel, + osUrl, + osOptions, + onDownloadUrlChange, }) => { const { t } = useTranslation(); const [selectedOsUrl, setSelectedOsUrl] = React.useState(osUrl); @@ -29,37 +29,41 @@ export const DesktopInstallTitle: React.FC = ({ setSelectedOsUrl(osUrl); }, [osUrl]); - const handleOsSelect = React.useCallback((option: OSOption) => { - setSelectedOsUrl(option.url); - onDownloadUrlChange?.(option.url); - }, [onDownloadUrlChange]); + const handleOsSelect = React.useCallback( + (option: OSOption) => { + setSelectedOsUrl(option.url); + onDownloadUrlChange?.(option.url); + }, + [onDownloadUrlChange], + ); - const currentOsOption = osOptions.find(opt => opt.url === selectedOsUrl) || + const currentOsOption = + osOptions.find((opt) => opt.url === selectedOsUrl) || (osOptions.length > 0 ? osOptions[0] : { label: osLabel, url: osUrl }); - + const displayLabel = currentOsOption.label || osLabel; - const title = displayLabel - ? t('onboarding.desktopInstall.titleWithOs', 'Download for {{osLabel}}', { osLabel: displayLabel }) - : t('onboarding.desktopInstall.title', 'Download'); + const title = displayLabel + ? t("onboarding.desktopInstall.titleWithOs", "Download for {{osLabel}}", { osLabel: displayLabel }) + : t("onboarding.desktopInstall.title", "Download"); // If only one option or no options, don't show dropdown if (osOptions.length <= 1) { - return
{title}
; + return
{title}
; } return ( -
- {title} +
+ {title} @@ -74,11 +78,9 @@ export const DesktopInstallTitle: React.FC = ({ onClick={() => handleOsSelect(option)} style={{ backgroundColor: isSelected - ? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))' - : 'transparent', - color: isSelected - ? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))' - : 'inherit', + ? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))" + : "transparent", + color: isSelected ? "light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))" : "inherit", }} > {option.label} @@ -90,4 +92,3 @@ export const DesktopInstallTitle: React.FC = ({
); }; - diff --git a/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx b/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx index 46ec6a0c89..df7d5ff544 100644 --- a/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react'; -import { Stack, PasswordInput, Button, Alert, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { SlideConfig } from '@app/types/types'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import { accountService } from '@app/services/accountService'; -import { alert as showToast } from '@app/components/toast'; -import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css'; +import React, { useState } from "react"; +import { Stack, PasswordInput, Button, Alert, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { SlideConfig } from "@app/types/types"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import { accountService } from "@app/services/accountService"; +import { alert as showToast } from "@app/components/toast"; +import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css"; interface FirstLoginSlideProps { username: string; @@ -14,66 +14,66 @@ interface FirstLoginSlideProps { usingDefaultCredentials?: boolean; } -const DEFAULT_PASSWORD = 'stirling'; +const DEFAULT_PASSWORD = "stirling"; function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = false }: FirstLoginSlideProps) { const { t } = useTranslation(); // If using default credentials, pre-fill with "stirling" - user won't see this field - const [currentPassword, setCurrentPassword] = useState(usingDefaultCredentials ? DEFAULT_PASSWORD : ''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); + const [currentPassword, setCurrentPassword] = useState(usingDefaultCredentials ? DEFAULT_PASSWORD : ""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const handleSubmit = async () => { // Validation if ((!usingDefaultCredentials && !currentPassword) || !newPassword || !confirmPassword) { - setError(t('firstLogin.allFieldsRequired', 'All fields are required')); + setError(t("firstLogin.allFieldsRequired", "All fields are required")); return; } if (newPassword !== confirmPassword) { - setError(t('firstLogin.passwordsDoNotMatch', 'New passwords do not match')); + setError(t("firstLogin.passwordsDoNotMatch", "New passwords do not match")); return; } if (newPassword.length < 8) { - setError(t('firstLogin.passwordTooShort', 'Password must be at least 8 characters')); + setError(t("firstLogin.passwordTooShort", "Password must be at least 8 characters")); return; } if (newPassword === currentPassword) { - setError(t('firstLogin.passwordMustBeDifferent', 'New password must be different from current password')); + setError(t("firstLogin.passwordMustBeDifferent", "New password must be different from current password")); return; } try { setLoading(true); - setError(''); + setError(""); await accountService.changePasswordOnLogin(currentPassword, newPassword, confirmPassword); showToast({ - alertType: 'success', - title: t('firstLogin.passwordChangedSuccess', 'Password changed successfully! Please log in again.') + alertType: "success", + title: t("firstLogin.passwordChangedSuccess", "Password changed successfully! Please log in again."), }); // Clear form - setCurrentPassword(''); - setNewPassword(''); - setConfirmPassword(''); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); // Wait a moment for the user to see the success message setTimeout(() => { onPasswordChanged(); }, 1500); } catch (err) { - console.error('Failed to change password:', err); + console.error("Failed to change password:", err); // Extract error message from axios response if available const axiosError = err as { response?: { data?: { message?: string } } }; setError( axiosError.response?.data?.message || - t('firstLogin.passwordChangeFailed', 'Failed to change password. Please check your current password.') + t("firstLogin.passwordChangeFailed", "Failed to change password. Please check your current password."), ); } finally { setLoading(false); @@ -85,25 +85,18 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials =
- + - {t( - 'firstLogin.welcomeMessage', - 'For security reasons, you must change your password on your first login.' - )} + {t("firstLogin.welcomeMessage", "For security reasons, you must change your password on your first login.")}
- {t('firstLogin.loggedInAs', 'Logged in as')}: {username} + {t("firstLogin.loggedInAs", "Logged in as")}: {username} {error && ( - } - color="red" - variant="light" - > + } color="red" variant="light"> {error} )} @@ -111,8 +104,8 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = {/* Only show current password field if not using default credentials */} {!usingDefaultCredentials && ( setCurrentPassword(e.currentTarget.value)} required @@ -123,8 +116,8 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = )} setNewPassword(e.currentTarget.value)} minLength={8} @@ -135,8 +128,8 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = /> setConfirmPassword(e.currentTarget.value)} required @@ -154,7 +147,7 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = size="md" mt="xs" > - {t('firstLogin.changePassword', 'Change Password')} + {t("firstLogin.changePassword", "Change Password")}
@@ -168,8 +161,8 @@ export default function FirstLoginSlide({ usingDefaultCredentials = false, }: FirstLoginSlideProps): SlideConfig { return { - key: 'first-login', - title: 'Set Your Password', + key: "first-login", + title: "Set Your Password", body: ( ), background: { - gradientStops: ['#059669', '#0891B2'], // Green to teal - security/trust colors + gradientStops: ["#059669", "#0891B2"], // Green to teal - security/trust colors circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx b/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx index d8def0528a..b565af0f94 100644 --- a/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx @@ -4,7 +4,7 @@ import { QRCodeSVG } from "qrcode.react"; import { SlideConfig } from "@app/types/types"; import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; import { accountService } from "@app/services/accountService"; -import { useAccountLogout } from '@app/extensions/accountLogout'; +import { useAccountLogout } from "@app/extensions/accountLogout"; import { useAuth } from "@app/auth/UseSession"; import LocalIcon from "@app/components/shared/LocalIcon"; import { BASE_PATH } from "@app/constants/app"; @@ -59,10 +59,10 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { }, [fetchMfaSetup]); const redirectToLogin = useCallback(() => { - window.location.assign('/login'); + window.location.assign("/login"); }, []); - const onLogout = useCallback(async() => { + const onLogout = useCallback(async () => { await accountLogout({ signOut, redirectToLogin }); }, [accountLogout, redirectToLogin, signOut]); @@ -84,13 +84,13 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { } catch (err) { const axiosError = err as { response?: { data?: { error?: string } } }; setMfaError( - axiosError.response?.data?.error || "Unable to enable two-factor authentication. Check the code and try again." + axiosError.response?.data?.error || "Unable to enable two-factor authentication. Check the code and try again.", ); } finally { setSubmitting(false); } }, - [mfaSetupCode, onMfaSetupComplete] + [mfaSetupCode, onMfaSetupComplete], ); const isReady = Boolean(mfaSetupData); @@ -179,18 +179,10 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { > Regenerate QR code - - diff --git a/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx b/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx index 3b8d4bfb0c..cb86b2062c 100644 --- a/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { SlideConfig, LicenseNotice } from '@app/types/types'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; +import React from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { SlideConfig, LicenseNotice } from "@app/types/types"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; interface PlanOverviewSlideProps { isAdmin: boolean; @@ -16,31 +16,23 @@ const PlanOverviewTitle: React.FC<{ isAdmin: boolean }> = ({ isAdmin }) => { return ( <> {isAdmin - ? t('onboarding.planOverview.adminTitle', 'Admin Overview') - : t('onboarding.planOverview.userTitle', 'Plan Overview')} + ? t("onboarding.planOverview.adminTitle", "Admin Overview") + : t("onboarding.planOverview.userTitle", "Plan Overview")} ); }; -const AdminOverviewBody: React.FC<{ freeTierLimit: number; loginEnabled: boolean }> = ({ - freeTierLimit, - loginEnabled, -}) => { +const AdminOverviewBody: React.FC<{ freeTierLimit: number; loginEnabled: boolean }> = ({ freeTierLimit, loginEnabled }) => { const adminBodyKey = loginEnabled - ? 'onboarding.planOverview.adminBodyLoginEnabled' - : 'onboarding.planOverview.adminBodyLoginDisabled'; + ? "onboarding.planOverview.adminBodyLoginEnabled" + : "onboarding.planOverview.adminBodyLoginDisabled"; const defaultValue = loginEnabled - ? 'As an admin, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge.' - : 'Once you enable login mode, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge.'; + ? "As an admin, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge." + : "Once you enable login mode, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge."; return ( - }} - defaults={defaultValue} - /> + }} defaults={defaultValue} /> ); }; @@ -49,7 +41,7 @@ const UserOverviewBody: React.FC = () => { return ( {t( - 'onboarding.planOverview.userBody', + "onboarding.planOverview.userBody", "Invite teammates, assign roles, and keep your documents organized in one secure workspace. Enable login mode whenever you're ready to grow beyond solo use.", )} @@ -60,8 +52,7 @@ const PlanOverviewBody: React.FC<{ isAdmin: boolean; freeTierLimit: number; logi isAdmin, freeTierLimit, loginEnabled, -}) => - isAdmin ? : ; +}) => (isAdmin ? : ); export default function PlanOverviewSlide({ isAdmin, @@ -71,13 +62,12 @@ export default function PlanOverviewSlide({ const freeTierLimit = licenseNotice?.freeTierLimit ?? DEFAULT_FREE_TIER_LIMIT; return { - key: isAdmin ? 'admin-overview' : 'plan-overview', + key: isAdmin ? "admin-overview" : "plan-overview", title: , body: , background: { - gradientStops: isAdmin ? ['#4F46E5', '#0EA5E9'] : ['#F97316', '#EF4444'], + gradientStops: isAdmin ? ["#4F46E5", "#0EA5E9"] : ["#F97316", "#EF4444"], circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx b/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx index 0efb2f591d..f0245e6cd9 100644 --- a/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx @@ -1,39 +1,41 @@ -import React from 'react'; -import { Select } from '@mantine/core'; -import { SlideConfig } from '@app/types/types'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import i18n from '@app/i18n'; -import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css'; +import React from "react"; +import { Select } from "@mantine/core"; +import { SlideConfig } from "@app/types/types"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import i18n from "@app/i18n"; +import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css"; interface SecurityCheckSlideProps { - selectedRole: 'admin' | 'user' | null; - onRoleSelect: (role: 'admin' | 'user' | null) => void; + selectedRole: "admin" | "user" | null; + onRoleSelect: (role: "admin" | "user" | null) => void; } -export default function SecurityCheckSlide({ - selectedRole, - onRoleSelect, -}: SecurityCheckSlideProps): SlideConfig { +export default function SecurityCheckSlide({ selectedRole, onRoleSelect }: SecurityCheckSlideProps): SlideConfig { return { - key: 'security-check', - title: 'Security Check', + key: "security-check", + title: "Security Check", body: (
- - {i18n.t('onboarding.securityCheck.message', 'The application has undergone significant changes recently. Your server admin\'s attention may be required. Please confirm your role to continue.')} + + + {i18n.t( + "onboarding.securityCheck.message", + "The application has undergone significant changes recently. Your server admin's attention may be required. Please confirm your role to continue.", + )} +
setShareRole((value as typeof shareRole) || 'editor')} + onChange={(value) => setShareRole((value as typeof shareRole) || "editor")} comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10 }} data={[ - { value: 'editor', label: t('storageShare.roleEditor', 'Editor') }, - { value: 'commenter', label: t('storageShare.roleCommenter', 'Commenter') }, - { value: 'viewer', label: t('storageShare.roleViewer', 'Viewer') }, + { value: "editor", label: t("storageShare.roleEditor", "Editor") }, + { value: "commenter", label: t("storageShare.roleCommenter", "Commenter") }, + { value: "viewer", label: t("storageShare.roleViewer", "Viewer") }, ]} /> - {shareRole === 'commenter' && ( + {shareRole === "commenter" && ( - {t('storageShare.commenterHint', 'Commenting is coming soon.')} + {t("storageShare.commenterHint", "Commenting is coming soon.")} )} @@ -245,7 +228,7 @@ const BulkShareModal: React.FC = ({ diff --git a/frontend/src/core/components/shared/BulkUploadToServerModal.tsx b/frontend/src/core/components/shared/BulkUploadToServerModal.tsx index 4e97fe74c7..e9fb4650c1 100644 --- a/frontend/src/core/components/shared/BulkUploadToServerModal.tsx +++ b/frontend/src/core/components/shared/BulkUploadToServerModal.tsx @@ -1,15 +1,15 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Modal, Stack, Text, Button, Group, Alert } from '@mantine/core'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Modal, Stack, Text, Button, Group, Alert } from "@mantine/core"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import { useTranslation } from "react-i18next"; -import { alert } from '@app/components/toast'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { uploadHistoryChains } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; +import { alert } from "@app/components/toast"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { uploadHistoryChains } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; interface BulkUploadToServerModalProps { opened: boolean; @@ -18,12 +18,7 @@ interface BulkUploadToServerModalProps { onUploaded?: () => Promise | void; } -const BulkUploadToServerModal: React.FC = ({ - opened, - onClose, - files, - onUploaded, -}) => { +const BulkUploadToServerModal: React.FC = ({ opened, onClose, files, onUploaded }) => { const { t } = useTranslation(); const { actions } = useFileActions(); const [isUploading, setIsUploading] = useState(false); @@ -44,18 +39,11 @@ const BulkUploadToServerModal: React.FC = ({ setErrorMessage(null); try { - const rootIds = Array.from( - new Set(files.map((file) => (file.originalFileId || file.id) as FileId)) - ); - const remoteIds = Array.from( - new Set(files.map((file) => file.remoteStorageId).filter(Boolean) as number[]) - ); + const rootIds = Array.from(new Set(files.map((file) => (file.originalFileId || file.id) as FileId))); + const remoteIds = Array.from(new Set(files.map((file) => file.remoteStorageId).filter(Boolean) as number[])); const existingRemoteId = remoteIds.length === 1 ? remoteIds[0] : undefined; - const { remoteId, updatedAt, chain } = await uploadHistoryChains( - rootIds, - existingRemoteId - ); + const { remoteId, updatedAt, chain } = await uploadHistoryChains(rootIds, existingRemoteId); for (const stub of chain) { actions.updateStirlingFileStub(stub.id, { @@ -73,8 +61,8 @@ const BulkUploadToServerModal: React.FC = ({ } alert({ - alertType: 'success', - title: t('storageUpload.success', 'Uploaded to server'), + alertType: "success", + title: t("storageUpload.success", "Uploaded to server"), expandable: false, durationMs: 3000, }); @@ -83,10 +71,8 @@ const BulkUploadToServerModal: React.FC = ({ } onClose(); } catch (error) { - console.error('Failed to upload files to server:', error); - setErrorMessage( - t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.') - ); + console.error("Failed to upload files to server:", error); + setErrorMessage(t("storageUpload.failure", "Upload failed. Please check your login and storage settings.")); } finally { setIsUploading(false); } @@ -97,48 +83,39 @@ const BulkUploadToServerModal: React.FC = ({ opened={opened} onClose={onClose} centered - title={t('storageUpload.bulkTitle', 'Upload selected files')} + title={t("storageUpload.bulkTitle", "Upload selected files")} zIndex={Z_INDEX_OVER_FILE_MANAGER_MODAL} > - - {t( - 'storageUpload.bulkDescription', - 'This uploads the selected files to your server storage.' - )} - + {t("storageUpload.bulkDescription", "This uploads the selected files to your server storage.")} - {t('storageUpload.fileCount', '{{count}} files selected', { + {t("storageUpload.fileCount", "{{count}} files selected", { count: files.length, })} {displayNames.length > 0 && ( - {displayNames.join(', ')} + {displayNames.join(", ")} {fileNames.length > displayNames.length - ? t('storageUpload.more', ' +{{count}} more', { + ? t("storageUpload.more", " +{{count}} more", { count: fileNames.length - displayNames.length, }) - : ''} + : ""} )} {errorMessage && ( - + {errorMessage} )} - diff --git a/frontend/src/core/components/shared/ButtonSelector.test.tsx b/frontend/src/core/components/shared/ButtonSelector.test.tsx index 12a509abd6..d0715a3090 100644 --- a/frontend/src/core/components/shared/ButtonSelector.test.tsx +++ b/frontend/src/core/components/shared/ButtonSelector.test.tsx @@ -1,214 +1,175 @@ -import { describe, expect, test, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { MantineProvider } from '@mantine/core'; -import ButtonSelector from '@app/components/shared/ButtonSelector'; +import { describe, expect, test, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import ButtonSelector from "@app/components/shared/ButtonSelector"; // Wrapper component to provide Mantine context -const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - {children} -); +const TestWrapper = ({ children }: { children: React.ReactNode }) => {children}; -describe('ButtonSelector', () => { +describe("ButtonSelector", () => { const mockOnChange = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); - test('should render all options as buttons', () => { + test("should render all options as buttons", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( - - + + , ); - expect(screen.getByText('Test Label')).toBeInTheDocument(); - expect(screen.getByText('Option 1')).toBeInTheDocument(); - expect(screen.getByText('Option 2')).toBeInTheDocument(); + expect(screen.getByText("Test Label")).toBeInTheDocument(); + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); }); - test('should highlight selected button with filled variant', () => { + test("should highlight selected button with filled variant", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( - - + + , ); - const selectedButton = screen.getByRole('button', { name: 'Option 1' }); - const unselectedButton = screen.getByRole('button', { name: 'Option 2' }); + const selectedButton = screen.getByRole("button", { name: "Option 1" }); + const unselectedButton = screen.getByRole("button", { name: "Option 2" }); // Check data-variant attribute for filled/outline - expect(selectedButton).toHaveAttribute('data-variant', 'filled'); - expect(unselectedButton).toHaveAttribute('data-variant', 'outline'); - expect(screen.getByText('Selection Label')).toBeInTheDocument(); + expect(selectedButton).toHaveAttribute("data-variant", "filled"); + expect(unselectedButton).toHaveAttribute("data-variant", "outline"); + expect(screen.getByText("Selection Label")).toBeInTheDocument(); }); - test('should call onChange when button is clicked', () => { + test("should call onChange when button is clicked", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( - - + + , ); - fireEvent.click(screen.getByRole('button', { name: 'Option 2' })); + fireEvent.click(screen.getByRole("button", { name: "Option 2" })); - expect(mockOnChange).toHaveBeenCalledWith('option2'); + expect(mockOnChange).toHaveBeenCalledWith("option2"); }); - test('should handle undefined value (no selection)', () => { + test("should handle undefined value (no selection)", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( - - + + , ); // Both buttons should be outlined when no value is selected - const button1 = screen.getByRole('button', { name: 'Option 1' }); - const button2 = screen.getByRole('button', { name: 'Option 2' }); + const button1 = screen.getByRole("button", { name: "Option 1" }); + const button2 = screen.getByRole("button", { name: "Option 2" }); - expect(button1).toHaveAttribute('data-variant', 'outline'); - expect(button2).toHaveAttribute('data-variant', 'outline'); + expect(button1).toHaveAttribute("data-variant", "outline"); + expect(button2).toHaveAttribute("data-variant", "outline"); }); test.each([ { - description: 'disable buttons when disabled prop is true', + description: "disable buttons when disabled prop is true", options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ], globalDisabled: true, expectedStates: [true, true], }, { - description: 'disable individual options when option.disabled is true', + description: "disable individual options when option.disabled is true", options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2', disabled: true }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2", disabled: true }, ], globalDisabled: false, expectedStates: [false, true], }, - ])('should $description', ({ options, globalDisabled, expectedStates }) => { + ])("should $description", ({ options, globalDisabled, expectedStates }) => { render( - - + + , ); options.forEach((option, index) => { - const button = screen.getByRole('button', { name: option.label }); - expect(button).toHaveProperty('disabled', expectedStates[index]); + const button = screen.getByRole("button", { name: option.label }); + expect(button).toHaveProperty("disabled", expectedStates[index]); }); }); - test('should not call onChange when disabled button is clicked', () => { + test("should not call onChange when disabled button is clicked", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2', disabled: true }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2", disabled: true }, ]; render( - - + + , ); - fireEvent.click(screen.getByRole('button', { name: 'Option 2' })); + fireEvent.click(screen.getByRole("button", { name: "Option 2" })); expect(mockOnChange).not.toHaveBeenCalled(); }); - test('should not apply fullWidth styling when fullWidth is false', () => { + test("should not apply fullWidth styling when fullWidth is false", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( - - + + , ); - const button = screen.getByRole('button', { name: 'Option 1' }); - expect(button).not.toHaveStyle({ flex: '1' }); - expect(screen.getByText('Layout Label')).toBeInTheDocument(); + const button = screen.getByRole("button", { name: "Option 1" }); + expect(button).not.toHaveStyle({ flex: "1" }); + expect(screen.getByText("Layout Label")).toBeInTheDocument(); }); - test('should not render label element when not provided', () => { + test("should not render label element when not provided", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; const { container } = render( - - + + , ); // Should render buttons - expect(screen.getByText('Option 1')).toBeInTheDocument(); - expect(screen.getByText('Option 2')).toBeInTheDocument(); - + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + // Stack should only contain the Group (buttons), no Text element for label const stackElement = container.querySelector('[class*="mantine-Stack-root"]'); expect(stackElement?.children).toHaveLength(1); // Only the Group, no label Text diff --git a/frontend/src/core/components/shared/ButtonSelector.tsx b/frontend/src/core/components/shared/ButtonSelector.tsx index 94bd10c6e7..548d75fd81 100644 --- a/frontend/src/core/components/shared/ButtonSelector.tsx +++ b/frontend/src/core/components/shared/ButtonSelector.tsx @@ -5,7 +5,7 @@ export interface ButtonOption { value: T; label: string; disabled?: boolean; - tooltip?: string; // Tooltip shown on hover (useful for explaining why option is disabled) + tooltip?: string; // Tooltip shown on hover (useful for explaining why option is disabled) } interface ButtonSelectorProps { @@ -30,42 +30,42 @@ const ButtonSelector = ({ textClassName, }: ButtonSelectorProps) => { return ( - + {/* Label (if it exists) */} - {label && {label}} + {label && ( + + {label} + + )} {/* Buttons */} - + {options.map((option) => { const isDisabled = disabled || option.disabled; const button = ( ); @@ -73,12 +73,16 @@ const ButtonSelector = ({ if (option.tooltip && isDisabled) { return ( - {button} + {button} ); } - return {button}; + return ( + + {button} + + ); })} diff --git a/frontend/src/core/components/shared/ButtonToggle.tsx b/frontend/src/core/components/shared/ButtonToggle.tsx index f695c8a8bc..2f434e0086 100644 --- a/frontend/src/core/components/shared/ButtonToggle.tsx +++ b/frontend/src/core/components/shared/ButtonToggle.tsx @@ -1,5 +1,5 @@ -import { Button, Stack } from '@mantine/core'; -import React from 'react'; +import { Button, Stack } from "@mantine/core"; +import React from "react"; export interface ButtonToggleOption { value: string; @@ -13,8 +13,8 @@ export interface ButtonToggleProps { value: string; onChange: (value: string) => void; disabled?: boolean; - orientation?: 'vertical' | 'horizontal'; - size?: 'xs' | 'sm' | 'md' | 'lg'; + orientation?: "vertical" | "horizontal"; + size?: "xs" | "sm" | "md" | "lg"; fullWidth?: boolean; } @@ -23,18 +23,18 @@ export const ButtonToggle: React.FC = ({ value, onChange, disabled = false, - orientation = 'vertical', - size = 'md', + orientation = "vertical", + size = "md", fullWidth = true, }) => { - const isVertical = orientation === 'vertical'; + const isVertical = orientation === "vertical"; const buttonStyle: React.CSSProperties = { - justifyContent: 'flex-start', - height: isVertical ? 'auto' : undefined, - minHeight: isVertical ? '50px' : undefined, - padding: isVertical ? '12px 16px' : undefined, - textAlign: 'left', + justifyContent: "flex-start", + height: isVertical ? "auto" : undefined, + minHeight: isVertical ? "50px" : undefined, + padding: isVertical ? "12px 16px" : undefined, + textAlign: "left", }; const renderButton = (option: ButtonToggleOption) => { @@ -44,21 +44,21 @@ export const ButtonToggle: React.FC = ({ return ( ); diff --git a/frontend/src/core/components/shared/DropdownListWithFooter.tsx b/frontend/src/core/components/shared/DropdownListWithFooter.tsx index b5e5a9f5d7..11d457ef99 100644 --- a/frontend/src/core/components/shared/DropdownListWithFooter.tsx +++ b/frontend/src/core/components/shared/DropdownListWithFooter.tsx @@ -1,8 +1,8 @@ -import React, { ReactNode, useState, useMemo } from 'react'; -import { Stack, Text, Popover, Box, Checkbox, Group, TextInput } from '@mantine/core'; -import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; -import SearchIcon from '@mui/icons-material/Search'; -import { Z_INDEX_AUTOMATE_DROPDOWN } from '@app/styles/zIndex'; +import React, { ReactNode, useState, useMemo } from "react"; +import { Stack, Text, Popover, Box, Checkbox, Group, TextInput } from "@mantine/core"; +import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; +import SearchIcon from "@mui/icons-material/Search"; +import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex"; export interface DropdownItem { value: string; @@ -15,30 +15,30 @@ export interface DropdownListWithFooterProps { // Value and onChange - support both single and multi-select value: string | string[]; onChange: (value: string | string[]) => void; - + // Items and display items: DropdownItem[]; placeholder?: string; disabled?: boolean; - + // Labels and headers label?: string; header?: ReactNode; footer?: ReactNode; - + // Behavior multiSelect?: boolean; searchable?: boolean; maxHeight?: number; - + // Styling className?: string; dropdownClassName?: string; - + // Popover props - position?: 'top' | 'bottom' | 'left' | 'right'; + position?: "top" | "bottom" | "left" | "right"; withArrow?: boolean; - width?: 'target' | number; + width?: "target" | number; withinPortal?: boolean; zIndex?: number; } @@ -47,7 +47,7 @@ const DropdownListWithFooter: React.FC = ({ value, onChange, items, - placeholder = 'Select option', + placeholder = "Select option", disabled = false, label, header, @@ -55,34 +55,31 @@ const DropdownListWithFooter: React.FC = ({ multiSelect = false, searchable = false, maxHeight = 300, - className = '', - dropdownClassName = '', - position = 'bottom', + className = "", + dropdownClassName = "", + position = "bottom", withArrow = false, - width = 'target', + width = "target", withinPortal = true, - zIndex = Z_INDEX_AUTOMATE_DROPDOWN + zIndex = Z_INDEX_AUTOMATE_DROPDOWN, }) => { - - const [searchTerm, setSearchTerm] = useState(''); - + const [searchTerm, setSearchTerm] = useState(""); + const isMultiValue = Array.isArray(value); - const selectedValues = isMultiValue ? value : (value ? [value] : []); + const selectedValues = isMultiValue ? value : value ? [value] : []; // Filter items based on search term const filteredItems = useMemo(() => { if (!searchable || !searchTerm.trim()) { return items; } - return items.filter(item => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + return items.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase())); }, [items, searchTerm, searchable]); const handleItemClick = (itemValue: string) => { if (multiSelect) { const newSelection = selectedValues.includes(itemValue) - ? selectedValues.filter(v => v !== itemValue) + ? selectedValues.filter((v) => v !== itemValue) : [...selectedValues, itemValue]; onChange(newSelection); } else { @@ -94,7 +91,7 @@ const DropdownListWithFooter: React.FC = ({ if (selectedValues.length === 0) { return placeholder; } else if (selectedValues.length === 1) { - const selectedItem = items.find(item => item.value === selectedValues[0]); + const selectedItem = items.find((item) => item.value === selectedValues[0]); return selectedItem?.name || selectedValues[0]; } else { return `${selectedValues.length} selected`; @@ -112,125 +109,130 @@ const DropdownListWithFooter: React.FC = ({ {label} )} - - searchable && setSearchTerm('')} + onClose={() => searchable && setSearchTerm("")} withinPortal={withinPortal} zIndex={zIndex} > {getDisplayText()} - + - + {header && ( - + {header} )} - + {searchable && ( - + } + leftSection={} size="sm" - style={{ width: '100%' }} + style={{ width: "100%" }} /> )} - - + + {filteredItems.length === 0 ? ( - + - {searchable && searchTerm ? 'No results found' : 'No items available'} + {searchable && searchTerm ? "No results found" : "No items available"} ) : ( filteredItems.map((item) => ( - !item.disabled && handleItemClick(item.value)} - style={{ - padding: '8px 12px', - cursor: item.disabled ? 'not-allowed' : 'pointer', - borderRadius: 'var(--mantine-radius-sm)', - opacity: item.disabled ? 0.5 : 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between' - }} - onMouseEnter={(e) => { - if (!item.disabled) { - e.currentTarget.style.backgroundColor = 'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5))'; - } - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }} - > - - {item.leftIcon && ( - - {item.leftIcon} - + !item.disabled && handleItemClick(item.value)} + style={{ + padding: "8px 12px", + cursor: item.disabled ? "not-allowed" : "pointer", + borderRadius: "var(--mantine-radius-sm)", + opacity: item.disabled ? 0.5 : 1, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }} + onMouseEnter={(e) => { + if (!item.disabled) { + e.currentTarget.style.backgroundColor = + "light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5))"; + } + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = "transparent"; + }} + > + + {item.leftIcon && {item.leftIcon}} + {item.name} + + + {multiSelect && ( + {}} // Handled by parent onClick + size="sm" + disabled={item.disabled} + /> )} - {item.name} - - - {multiSelect && ( - {}} // Handled by parent onClick - size="sm" - disabled={item.disabled} - /> - )} - + )) )} - + {footer && ( - + {footer} )} @@ -241,4 +243,4 @@ const DropdownListWithFooter: React.FC = ({ ); }; -export default DropdownListWithFooter; \ No newline at end of file +export default DropdownListWithFooter; diff --git a/frontend/src/core/components/shared/EditableSecretField.tsx b/frontend/src/core/components/shared/EditableSecretField.tsx index dfd3da6458..927e46a73c 100644 --- a/frontend/src/core/components/shared/EditableSecretField.tsx +++ b/frontend/src/core/components/shared/EditableSecretField.tsx @@ -1,7 +1,7 @@ -import { useState, useRef, useEffect } from 'react'; -import { PasswordInput, Group, ActionIcon, Tooltip, TextInput } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { useState, useRef, useEffect } from "react"; +import { PasswordInput, Group, ActionIcon, Tooltip, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; interface EditableSecretFieldProps { label?: string; @@ -26,16 +26,16 @@ export default function EditableSecretField({ description, value, onChange, - placeholder = 'Enter value', + placeholder = "Enter value", disabled = false, error, }: EditableSecretFieldProps) { const { t } = useTranslation(); const [isEditing, setIsEditing] = useState(false); - const [tempValue, setTempValue] = useState(''); + const [tempValue, setTempValue] = useState(""); const inputRef = useRef(null); - const isMasked = value === '********'; + const isMasked = value === "********"; useEffect(() => { if (isEditing && inputRef.current) { @@ -44,45 +44,34 @@ export default function EditableSecretField({ }, [isEditing]); const handleEdit = () => { - setTempValue(''); + setTempValue(""); setIsEditing(true); }; const handleCancel = () => { - setTempValue(''); + setTempValue(""); setIsEditing(false); }; const handleSave = () => { - if (tempValue.trim() !== '') { + if (tempValue.trim() !== "") { onChange(tempValue); } - setTempValue(''); + setTempValue(""); setIsEditing(false); }; return (
- {label && } - {description &&

{description}

} + {label && } + {description &&

{description}

} {isMasked && !isEditing ? ( // Masked value from backend: show display + Edit button - - - + + + @@ -99,7 +88,7 @@ export default function EditableSecretField({ autoComplete="new-password" onBlur={handleSave} onKeyDown={(e) => { - if (e.key === 'Escape') handleCancel(); + if (e.key === "Escape") handleCancel(); }} /> ) : ( diff --git a/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx b/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx index 5ba2dbf053..765f90704b 100644 --- a/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx +++ b/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx @@ -1,7 +1,7 @@ -import { Modal, Stack, Text, Button, PasswordInput, Group } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { type KeyboardEventHandler } from 'react'; -import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; +import { Modal, Stack, Text, Button, PasswordInput, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { type KeyboardEventHandler } from "react"; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex"; interface EncryptedPdfUnlockModalProps { opened: boolean; @@ -27,7 +27,7 @@ const EncryptedPdfUnlockModal = ({ const { t } = useTranslation(); const handleKeyDown: KeyboardEventHandler = (event) => { - if (event.key === 'Enter' && !isProcessing && password.trim().length > 0) { + if (event.key === "Enter" && !isProcessing && password.trim().length > 0) { onUnlock(); } }; @@ -36,7 +36,7 @@ const EncryptedPdfUnlockModal = ({ - {fileName} + + {fileName} + {t( - 'encryptedPdfUnlock.description', - 'This PDF is password protected. Enter the password so you can continue working with it.' + "encryptedPdfUnlock.description", + "This PDF is password protected. Enter the password so you can continue working with it.", )} onPasswordChange(event.currentTarget.value)} onKeyDown={handleKeyDown} @@ -71,10 +73,10 @@ const EncryptedPdfUnlockModal = ({ diff --git a/frontend/src/core/components/shared/ErrorBoundary.tsx b/frontend/src/core/components/shared/ErrorBoundary.tsx index 0bab94f0a2..5b075c6a93 100644 --- a/frontend/src/core/components/shared/ErrorBoundary.tsx +++ b/frontend/src/core/components/shared/ErrorBoundary.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Text, Button, Stack } from '@mantine/core'; +import React from "react"; +import { Text, Button, Stack } from "@mantine/core"; interface ErrorBoundaryState { hasError: boolean; @@ -8,7 +8,7 @@ interface ErrorBoundaryState { interface ErrorBoundaryProps { children: React.ReactNode; - fallback?: React.ComponentType<{error?: Error; retry: () => void}>; + fallback?: React.ComponentType<{ error?: Error; retry: () => void }>; } export default class ErrorBoundary extends React.Component { @@ -23,22 +23,22 @@ export default class ErrorBoundary extends React.Component { @@ -72,26 +72,36 @@ export default class ErrorBoundary extends React.Component - Something went wrong - {process.env.NODE_ENV === 'development' && this.state.error && ( + + + Something went wrong + + {process.env.NODE_ENV === "development" && this.state.error && ( <> - + {this.state.error.message} {this.state.error.stack && ( -
- - Show stack trace +
+ + + Show stack trace + -
+                  
                     {this.state.error.stack}
                   
diff --git a/frontend/src/core/components/shared/FileCard.tsx b/frontend/src/core/components/shared/FileCard.tsx index dda4791753..569345f9d5 100644 --- a/frontend/src/core/components/shared/FileCard.tsx +++ b/frontend/src/core/components/shared/FileCard.tsx @@ -22,7 +22,17 @@ interface FileCardProps { isSupported?: boolean; // Whether the file format is supported by the current tool } -const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => { +const FileCard = ({ + file, + fileStub, + onRemove, + onDoubleClick, + onView, + onEdit, + isSelected, + onSelect, + isSupported = true, +}: FileCardProps) => { const { t } = useTranslation(); // Use record thumbnail if available, otherwise fall back to IndexedDB lookup const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileStub); @@ -30,7 +40,7 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS const [isHovered, setIsHovered] = useState(false); // Show loading state during hydration: PDF file without thumbnail yet - const isPdf = file.type === 'application/pdf'; + const isPdf = file.type === "application/pdf"; const isHydrating = isPdf && !thumb && !isGenerating; return ( @@ -44,11 +54,11 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS minWidth: 180, maxWidth: 260, cursor: onDoubleClick && isSupported ? "pointer" : undefined, - position: 'relative', - border: isSelected ? '2px solid var(--mantine-color-blue-6)' : undefined, - backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : undefined, + position: "relative", + border: isSelected ? "2px solid var(--mantine-color-blue-6)" : undefined, + backgroundColor: isSelected ? "var(--mantine-color-blue-0)" : undefined, opacity: isSupported ? 1 : 0.5, - filter: isSupported ? 'none' : 'grayscale(50%)' + filter: isSupported ? "none" : "grayscale(50%)", }} onDoubleClick={onDoubleClick} onMouseEnter={() => setIsHovered(true)} @@ -69,22 +79,22 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS justifyContent: "center", margin: "0 auto", background: "#fafbfc", - position: 'relative' + position: "relative", }} > {/* Hover action buttons */} {isHovered && (onView || onEdit) && (
e.stopPropagation()} > @@ -121,26 +131,23 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS
)} {thumb ? ( - PDF thumbnail - ) : (isGenerating || isHydrating) ? ( + PDF thumbnail + ) : isGenerating || isHydrating ? ( - Loading... + + Loading... + ) : ( -
+
100 * 1024 * 1024 ? "orange" : "red"} @@ -151,7 +158,9 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS {file.size > 100 * 1024 * 1024 && ( - Large File + + Large File + )}
)} @@ -169,12 +178,7 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS {getFileDate(file)} {fileStub?.id && ( - } - > + }> DB )} diff --git a/frontend/src/core/components/shared/FileDropdownMenu.tsx b/frontend/src/core/components/shared/FileDropdownMenu.tsx index fcb1a6a266..cd35d43aa1 100644 --- a/frontend/src/core/components/shared/FileDropdownMenu.tsx +++ b/frontend/src/core/components/shared/FileDropdownMenu.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from '@mantine/core'; -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import CloseIcon from '@mui/icons-material/Close'; -import FitText from '@app/components/shared/FitText'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { FileId } from '@app/types/file'; -import { truncateCenter } from '@app/utils/textUtils'; +import React from "react"; +import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from "@mantine/core"; +import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import CloseIcon from "@mui/icons-material/Close"; +import FitText from "@app/components/shared/FitText"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { FileId } from "@app/types/file"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileDropdownMenuProps { displayName: string; @@ -31,7 +31,7 @@ export const FileDropdownMenu: React.FC = ({ return ( -
+
{switchingTo === "viewer" ? ( ) : ( @@ -41,22 +41,24 @@ export const FileDropdownMenu: React.FC = ({
- + {activeFiles.map((file, index) => { - const itemName = file?.name || 'Untitled'; + const itemName = file?.name || "Untitled"; const isActive = index === currentFileIndex; return ( = ({ onFileSelect?.(index); }} className="viewer-file-tab" - {...(isActive && { 'data-active': true })} + {...(isActive && { "data-active": true })} style={{ - justifyContent: 'flex-start', + justifyContent: "flex-start", }} > - -
+ +
diff --git a/frontend/src/core/components/shared/FileGrid.tsx b/frontend/src/core/components/shared/FileGrid.tsx index 5bd31008db..c9ec2fe1b5 100644 --- a/frontend/src/core/components/shared/FileGrid.tsx +++ b/frontend/src/core/components/shared/FileGrid.tsx @@ -24,7 +24,7 @@ interface FileGridProps { isFileSupported?: (fileName: string) => boolean; // Function to check if file is supported } -type SortOption = 'date' | 'name' | 'size'; +type SortOption = "date" | "name" | "size"; const FileGrid = ({ files, @@ -40,25 +40,23 @@ const FileGrid = ({ onShowAll, showingAll = false, onDeleteAll, - isFileSupported + isFileSupported, }: FileGridProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(""); - const [sortBy, setSortBy] = useState('date'); + const [sortBy, setSortBy] = useState("date"); // Filter files based on search term - const filteredFiles = files.filter(item => - item.file.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const filteredFiles = files.filter((item) => item.file.name.toLowerCase().includes(searchTerm.toLowerCase())); // Sort files const sortedFiles = [...filteredFiles].sort((a, b) => { switch (sortBy) { - case 'date': + case "date": return (b.file.lastModified || 0) - (a.file.lastModified || 0); - case 'name': + case "name": return a.file.name.localeCompare(b.file.name); - case 'size': + case "size": return (b.file.size || 0) - (a.file.size || 0); default: return 0; @@ -66,14 +64,12 @@ const FileGrid = ({ }); // Apply max display limit if specified - const displayFiles = maxDisplay && !showingAll - ? sortedFiles.slice(0, maxDisplay) - : sortedFiles; + const displayFiles = maxDisplay && !showingAll ? sortedFiles.slice(0, maxDisplay) : sortedFiles; const hasMoreFiles = maxDisplay && !showingAll && sortedFiles.length > maxDisplay; return ( - + {/* Search and Sort Controls */} {(showSearch || showSort || onDeleteAll) && ( @@ -91,9 +87,9 @@ const FileGrid = ({ {showSort && ( + ); } diff --git a/frontend/src/core/components/shared/LandingDocumentStack.tsx b/frontend/src/core/components/shared/LandingDocumentStack.tsx index 3fffd48602..b0a37a0f67 100644 --- a/frontend/src/core/components/shared/LandingDocumentStack.tsx +++ b/frontend/src/core/components/shared/LandingDocumentStack.tsx @@ -19,9 +19,9 @@ export function LandingDocumentStack() {
-
-
-
+
+
+
diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index 1d9b878f46..46675255f3 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -1,14 +1,14 @@ -import React, { useState } from 'react'; -import { Container } from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import { useTranslation } from 'react-i18next'; -import { useFileHandler } from '@app/hooks/useFileHandler'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import MobileUploadModal from '@app/components/shared/MobileUploadModal'; -import { openFilesFromDisk } from '@app/services/openFilesFromDisk'; -import { LandingDocumentStack } from '@app/components/shared/LandingDocumentStack'; -import { LandingActions } from '@app/components/shared/LandingActions'; -import '@app/components/shared/LandingPage.css'; +import React, { useState } from "react"; +import { Container } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import { useTranslation } from "react-i18next"; +import { useFileHandler } from "@app/hooks/useFileHandler"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import MobileUploadModal from "@app/components/shared/MobileUploadModal"; +import { openFilesFromDisk } from "@app/services/openFilesFromDisk"; +import { LandingDocumentStack } from "@app/components/shared/LandingDocumentStack"; +import { LandingActions } from "@app/components/shared/LandingActions"; +import "@app/components/shared/LandingPage.css"; const LandingPage = () => { const { t } = useTranslation(); @@ -36,7 +36,7 @@ const LandingPage = () => { if (files.length > 0) { await addFiles(files); } - event.target.value = ''; + event.target.value = ""; }; const handleFilesReceivedFromMobile = async (files: File[]) => { @@ -46,7 +46,7 @@ const LandingPage = () => { }; return ( - + { className="flex min-h-0 flex-1 cursor-default flex-col items-center justify-center border-none bg-transparent px-4 py-8 shadow-none outline-none" styles={{ root: { - border: 'none !important', - backgroundColor: 'transparent', - overflow: 'visible', - '&[data-accept]': { outline: '2px dashed var(--accent-interactive)', outlineOffset: 4 }, - '&[data-reject]': { outline: '2px dashed var(--mantine-color-red-6)', outlineOffset: 4 }, + border: "none !important", + backgroundColor: "transparent", + overflow: "visible", + "&[data-accept]": { outline: "2px dashed var(--accent-interactive)", outlineOffset: 4 }, + "&[data-reject]": { outline: "2px dashed var(--mantine-color-red-6)", outlineOffset: 4 }, }, - inner: { overflow: 'visible', display: 'flex', flexDirection: 'column', alignItems: 'center', width: '100%' }, + inner: { overflow: "visible", display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }, }} > -

{t('landing.heroTitle', 'Stirling PDF')}

-

{t('landing.heroSubtitle', 'Drop in or add an existing PDF to get started.')}

+

{t("landing.heroTitle", "Stirling PDF")}

+

{t("landing.heroSubtitle", "Drop in or add an existing PDF to get started.")}

['position']; + position?: React.ComponentProps["position"]; offset?: number; compact?: boolean; // icon-only trigger tooltip?: string; // tooltip text for compact mode @@ -48,12 +48,12 @@ const LanguageItem: React.FC = ({ rippleEffect, pendingLanguage, compact, - disabled = false + disabled = false, }) => { const { t } = useTranslation(); const labelText = option.label; - const comingSoonText = t('comingSoon', 'Coming soon'); + const comingSoonText = t("comingSoon", "Coming soon"); const label = disabled ? ( @@ -68,7 +68,7 @@ const LanguageItem: React.FC = ({ className={styles.languageItem} style={{ opacity: animationTriggered ? 1 : 0, - transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)', + transform: animationTriggered ? "translateY(0px)" : "translateY(8px)", transition: `opacity 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.01}s, transform 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.01}s`, }} > @@ -81,40 +81,42 @@ const LanguageItem: React.FC = ({ disabled={disabled} styles={{ root: { - borderRadius: '4px', - minHeight: '32px', - padding: '4px 8px', - justifyContent: 'flex-start', - position: 'relative', - overflow: 'hidden', + borderRadius: "4px", + minHeight: "32px", + padding: "4px 8px", + justifyContent: "flex-start", + position: "relative", + overflow: "hidden", backgroundColor: isSelected - ? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))' - : 'transparent', + ? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))" + : "transparent", color: disabled - ? 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))' + ? "light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))" : isSelected - ? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))' - : 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))', - transition: 'all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94)', - cursor: disabled ? 'not-allowed' : 'pointer', - '&:hover': !disabled ? { - backgroundColor: isSelected - ? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))' - : 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))', - transform: 'translateY(-1px)', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', - } : {} + ? "light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))" + : "light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))", + transition: "all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94)", + cursor: disabled ? "not-allowed" : "pointer", + "&:hover": !disabled + ? { + backgroundColor: isSelected + ? "light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))" + : "light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))", + transform: "translateY(-1px)", + boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)", + } + : {}, }, label: { - fontSize: '13px', + fontSize: "13px", fontWeight: isSelected ? 600 : 400, - textAlign: 'left', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - position: 'relative', + textAlign: "left", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + position: "relative", zIndex: 2, - } + }, }} > {label} @@ -122,16 +124,16 @@ const LanguageItem: React.FC = ({
@@ -155,10 +157,10 @@ const RippleStyles: React.FC = () => ( // Main component const LanguageSelector: React.FC = ({ - position = 'bottom-start', + position = "bottom-start", offset = 8, compact = false, - tooltip + tooltip, }) => { const { i18n, ready } = useTranslation(); const [opened, setOpened] = useState(false); @@ -183,8 +185,7 @@ const LanguageSelector: React.FC = ({ // Get the filtered list of supported languages from i18n // This respects server config (ui.languages) applied by AppConfigLoader - const allowedLanguages = (i18n.options.supportedLngs as string[] || []) - .filter(lang => lang !== 'cimode'); // Exclude i18next debug language + const allowedLanguages = ((i18n.options.supportedLngs as string[]) || []).filter((lang) => lang !== "cimode"); // Exclude i18next debug language const languageOptions: LanguageOption[] = Object.entries(supportedLanguages) .filter(([code]) => allowedLanguages.length === 0 || allowedLanguages.includes(code)) @@ -196,13 +197,9 @@ const LanguageSelector: React.FC = ({ // Calculate dropdown width and grid columns based on number of languages // 2-4: 300px/2 cols, 5-9: 400px/3 cols, 10+: 600px/4 cols - const dropdownWidth = languageOptions.length <= 4 ? 300 - : languageOptions.length <= 9 ? 400 - : 600; + const dropdownWidth = languageOptions.length <= 4 ? 300 : languageOptions.length <= 9 ? 400 : 600; - const gridColumns = languageOptions.length <= 4 ? 2 - : languageOptions.length <= 9 ? 3 - : 4; + const gridColumns = languageOptions.length <= 4 ? 2 : languageOptions.length <= 9 ? 3 : 4; const handleLanguageChange = (value: string, event: React.MouseEvent) => { // Create ripple effect at click position (only for button mode) @@ -229,16 +226,15 @@ const LanguageSelector: React.FC = ({ setTimeout(() => setRippleEffect(null), 50); // Force a full reload so RTL/LTR layout and tooltips re-evaluate correctly - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.location.reload(); } }, 150); }, 100); }; - const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] || - supportedLanguages['en-GB'] || - 'English'; // Fallback if supportedLanguages lookup fails + const currentLanguage = + supportedLanguages[i18n.language as keyof typeof supportedLanguages] || supportedLanguages["en-GB"] || "English"; // Fallback if supportedLanguages lookup fails // Hide the language selector if there's only one language option // (no point showing a selector when there's nothing to select) @@ -258,9 +254,9 @@ const LanguageSelector: React.FC = ({ zIndex={Z_INDEX_CONFIG_MODAL} withinPortal transitionProps={{ - transition: 'scale-y', + transition: "scale-y", duration: 120, - timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' + timingFunction: "cubic-bezier(0.25, 0.46, 0.45, 0.94)", }} > @@ -272,11 +268,11 @@ const LanguageSelector: React.FC = ({ title={!opened && tooltip ? tooltip : undefined} styles={{ root: { - color: 'var(--right-rail-icon)', - '&:hover': { - backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))', - } - } + color: "var(--right-rail-icon)", + "&:hover": { + backgroundColor: "light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))", + }, + }, }} > @@ -288,50 +284,45 @@ const LanguageSelector: React.FC = ({ leftSection={} styles={{ root: { - border: 'none', - color: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))', - transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)', - '&:hover': { - backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))', - } + border: "none", + color: "light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-1))", + transition: "background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)", + "&:hover": { + backgroundColor: "light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))", + }, }, - label: { fontSize: '12px', fontWeight: 500 } + label: { fontSize: "12px", fontWeight: 500 }, }} > - - {currentLanguage} - + {currentLanguage} )} -
- {languageOptions.map((option, index) => ( - handleLanguageChange(option.value, event)} - rippleEffect={rippleEffect} - pendingLanguage={pendingLanguage} - compact={compact} - disabled={false} - /> - ))} +
+ {languageOptions.map((option, index) => ( + handleLanguageChange(option.value, event)} + rippleEffect={rippleEffect} + pendingLanguage={pendingLanguage} + compact={compact} + disabled={false} + /> + ))}
diff --git a/frontend/src/core/components/shared/LocalIcon.tsx b/frontend/src/core/components/shared/LocalIcon.tsx index ff7ca493af..5b12dc1fd9 100644 --- a/frontend/src/core/components/shared/LocalIcon.tsx +++ b/frontend/src/core/components/shared/LocalIcon.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { addCollection, Icon } from '@iconify/react'; -import iconSet from '../../../assets/material-symbols-icons.json'; // eslint-disable-line no-restricted-imports -- Outside app paths +import React from "react"; +import { addCollection, Icon } from "@iconify/react"; +import iconSet from "../../../assets/material-symbols-icons.json"; // eslint-disable-line no-restricted-imports -- Outside app paths // Load icons synchronously at import time - guaranteed to be ready on first render let iconsLoaded = false; @@ -13,7 +13,7 @@ try { console.info(`✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`); } } catch { - console.info('ℹ️ Local icons not available - using CDN fallback'); + console.info("ℹ️ Local icons not available - using CDN fallback"); } interface LocalIconProps { @@ -30,17 +30,15 @@ interface LocalIconProps { */ export const LocalIcon: React.FC = ({ icon, width, height, style, ...props }) => { // Convert our icon naming convention to the local collection format - const iconName = icon.startsWith('material-symbols:') - ? icon - : `material-symbols:${icon}`; + const iconName = icon.startsWith("material-symbols:") ? icon : `material-symbols:${icon}`; // Development logging (only in dev mode) - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV === "development") { const logKey = `icon-${iconName}`; if (!sessionStorage.getItem(logKey)) { - const source = iconsLoaded ? 'local' : 'CDN'; + const source = iconsLoaded ? "local" : "CDN"; console.debug(`🎯 Icon: ${iconName} (${source})`); - sessionStorage.setItem(logKey, 'logged'); + sessionStorage.setItem(logKey, "logged"); } } @@ -48,10 +46,10 @@ export const LocalIcon: React.FC = ({ icon, width, height, style // Use width if provided, otherwise fall back to height const size = width || height; - if (size && typeof size === 'string') { + if (size && typeof size === "string") { // If it's a CSS unit string (like '1.5rem'), use it as fontSize iconStyle.fontSize = size; - } else if (typeof size === 'number') { + } else if (typeof size === "number") { // If it's a number, treat it as pixels iconStyle.fontSize = `${size}px`; } diff --git a/frontend/src/core/components/shared/MobileUploadModal.tsx b/frontend/src/core/components/shared/MobileUploadModal.tsx index 275b2cb0d9..4358003cac 100644 --- a/frontend/src/core/components/shared/MobileUploadModal.tsx +++ b/frontend/src/core/components/shared/MobileUploadModal.tsx @@ -1,16 +1,16 @@ -import { useEffect, useCallback, useState, useRef } from 'react'; -import { Modal, Stack, Text, Badge, Box, Alert } from '@mantine/core'; -import { QRCodeSVG } from 'qrcode.react'; -import { useTranslation } from 'react-i18next'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import InfoRoundedIcon from '@mui/icons-material/InfoRounded'; -import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; -import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; -import WarningRoundedIcon from '@mui/icons-material/WarningRounded'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { withBasePath } from '@app/constants/app'; -import { convertImageToPdf, isImageFile } from '@app/utils/imageToPdfUtils'; -import apiClient from '@app/services/apiClient'; +import { useEffect, useCallback, useState, useRef } from "react"; +import { Modal, Stack, Text, Badge, Box, Alert } from "@mantine/core"; +import { QRCodeSVG } from "qrcode.react"; +import { useTranslation } from "react-i18next"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; +import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; +import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; +import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { withBasePath } from "@app/constants/app"; +import { convertImageToPdf, isImageFile } from "@app/utils/imageToPdfUtils"; +import apiClient from "@app/services/apiClient"; interface MobileUploadModalProps { opened: boolean; @@ -21,9 +21,9 @@ interface MobileUploadModalProps { // Generate a cryptographically secure UUID v4-like session ID function generateSessionId(): string { // Use Web Crypto API for cryptographically secure random values - const cryptoObj = typeof crypto !== 'undefined' ? crypto : (window as any).crypto; + const cryptoObj = typeof crypto !== "undefined" ? crypto : (window as any).crypto; - if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') { + if (cryptoObj && typeof cryptoObj.getRandomValues === "function") { const bytes = new Uint8Array(16); cryptoObj.getRandomValues(bytes); @@ -32,19 +32,19 @@ function generateSessionId(): string { bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 // Convert bytes to hex string in UUID format - const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')); + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")); return [ - hex.slice(0, 4).join(''), - hex.slice(4, 6).join(''), - hex.slice(6, 8).join(''), - hex.slice(8, 10).join(''), - hex.slice(10, 16).join(''), - ].join('-'); + hex.slice(0, 4).join(""), + hex.slice(4, 6).join(""), + hex.slice(6, 8).join(""), + hex.slice(8, 10).join(""), + hex.slice(10, 16).join(""), + ].join("-"); } // If Web Crypto is not available, fail fast rather than using insecure randomness - console.error('Web Crypto API not available. Cannot generate secure session ID.'); - throw new Error('Web Crypto API not available. Cannot generate secure session ID.'); + console.error("Web Crypto API not available. Cannot generate secure session ID."); + throw new Error("Web Crypto API not available. Cannot generate secure session ID."); } interface SessionInfo { @@ -76,30 +76,37 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: // Use configured frontendUrl if set, otherwise use current origin // Combine with base path and mobile-scanner route - const baseUrl = localStorage.getItem('server_url') || ''; + const baseUrl = localStorage.getItem("server_url") || ""; const frontendUrl = baseUrl || config?.frontendUrl || window.location.origin; - const mobileUrl = `${frontendUrl}${withBasePath('/mobile-scanner')}?session=${sessionId}`; + const mobileUrl = `${frontendUrl}${withBasePath("/mobile-scanner")}?session=${sessionId}`; // Create session on backend - const createSession = useCallback(async (newSessionId: string) => { - try { - const response = await apiClient.post(`/api/v1/mobile-scanner/create-session/${newSessionId}`, undefined, { - responseType: 'json', - }); + const createSession = useCallback( + async (newSessionId: string) => { + try { + const response = await apiClient.post( + `/api/v1/mobile-scanner/create-session/${newSessionId}`, + undefined, + { + responseType: "json", + }, + ); - if (!response.status || response.status !== 200) { - throw new Error('Failed to create session'); + if (!response.status || response.status !== 200) { + throw new Error("Failed to create session"); + } + + const data = response.data; + setSessionInfo(data); + setError(null); + console.log("[MobileUploadModal] Session created:", data); + } catch (err) { + console.error("[MobileUploadModal] Failed to create session:", err); + setError(t("mobileUpload.sessionCreateError", "Failed to create session")); } - - const data = response.data; - setSessionInfo(data); - setError(null); - console.log('[MobileUploadModal] Session created:', data); - } catch (err) { - console.error('[MobileUploadModal] Failed to create session:', err); - setError(t('mobileUpload.sessionCreateError', 'Failed to create session')); - } - }, [t]); + }, + [t], + ); // Regenerate session (when expired or warned) const regenerateSession = useCallback(() => { @@ -117,7 +124,7 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: try { const response = await apiClient.get(`/api/v1/mobile-scanner/files/${sessionId}`); if (!response.status || response.status !== 200) { - throw new Error('Failed to check for files'); + throw new Error("Failed to check for files"); } const data = response.data; @@ -130,28 +137,29 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: for (const fileMetadata of newFiles) { try { const downloadResponse = await apiClient.get( - `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, { - responseType: 'blob', - } + `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, + { + responseType: "blob", + }, ); if (downloadResponse.status === 200) { const blob = downloadResponse.data; let file = new File([blob], fileMetadata.filename, { - type: fileMetadata.contentType || 'image/jpeg' + type: fileMetadata.contentType || "image/jpeg", }); // Convert images to PDF if enabled if (isImageFile(file) && config?.mobileScannerConvertToPdf !== false) { try { file = await convertImageToPdf(file, { - imageResolution: config?.mobileScannerImageResolution as 'full' | 'reduced' | undefined, - pageFormat: config?.mobileScannerPageFormat as 'keep' | 'A4' | 'letter' | undefined, + imageResolution: config?.mobileScannerImageResolution as "full" | "reduced" | undefined, + pageFormat: config?.mobileScannerPageFormat as "keep" | "A4" | "letter" | undefined, stretchToFit: config?.mobileScannerStretchToFit, }); - console.log('[MobileUploadModal] Converted image to PDF:', file.name); + console.log("[MobileUploadModal] Converted image to PDF:", file.name); } catch (convertError) { - console.warn('[MobileUploadModal] Failed to convert image to PDF, using original file:', convertError); + console.warn("[MobileUploadModal] Failed to convert image to PDF, using original file:", convertError); // Continue with original image file if conversion fails } } @@ -161,7 +169,7 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: onFilesReceived([file]); } } catch (err) { - console.error('[MobileUploadModal] Failed to download file:', fileMetadata.filename, err); + console.error("[MobileUploadModal] Failed to download file:", fileMetadata.filename, err); } } @@ -169,14 +177,14 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: // This ensures files are only on server for ~1 second try { await apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`); - console.log('[MobileUploadModal] Session cleaned up after file download'); + console.log("[MobileUploadModal] Session cleaned up after file download"); } catch (cleanupErr) { - console.warn('[MobileUploadModal] Failed to cleanup session after download:', cleanupErr); + console.warn("[MobileUploadModal] Failed to cleanup session after download:", cleanupErr); } } } catch (err) { - console.error('[MobileUploadModal] Error polling for files:', err); - setError(t('mobileUpload.pollingError', 'Error checking for files')); + console.error("[MobileUploadModal] Error polling for files:", err); + setError(t("mobileUpload.pollingError", "Error checking for files")); } }, [opened, sessionId, onFilesReceived, t]); @@ -201,9 +209,10 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: processedFiles.current.clear(); return () => { - console.log('Cleaning up session on unmount/close:', sessionId); - apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`) - .catch(err => console.warn('[MobileUploadModal] Cleanup failed:', err)); + console.log("Cleaning up session on unmount/close:", sessionId); + apiClient + .delete(`/api/v1/mobile-scanner/session/${sessionId}`) + .catch((err) => console.warn("[MobileUploadModal] Cleanup failed:", err)); }; }, [opened, sessionId, createSession]); @@ -267,7 +276,7 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: - } - color="blue" - variant="light" - > + } color="blue" variant="light"> {config?.mobileScannerConvertToPdf !== false ? t( - 'mobileUpload.description', - 'Scan this QR code with your mobile device to upload photos. Images will be automatically converted to PDF.' + "mobileUpload.description", + "Scan this QR code with your mobile device to upload photos. Images will be automatically converted to PDF.", ) - : t( - 'mobileUpload.descriptionNoConvert', - 'Scan this QR code with your mobile device to upload photos.' - )} + : t("mobileUpload.descriptionNoConvert", "Scan this QR code with your mobile device to upload photos.")} {showExpiryWarning && timeRemaining !== null && ( } - title={t('mobileUpload.expiryWarning', 'Session Expiring Soon')} + icon={} + title={t("mobileUpload.expiryWarning", "Session Expiring Soon")} color="orange" > {t( - 'mobileUpload.expiryWarningMessage', - 'This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.', - { seconds: Math.ceil(timeRemaining / 1000) } + "mobileUpload.expiryWarningMessage", + "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.", + { seconds: Math.ceil(timeRemaining / 1000) }, )} @@ -316,41 +318,41 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: {error && ( } - title={t('mobileUpload.error', 'Connection Error')} + icon={} + title={t("mobileUpload.error", "Connection Error")} color="red" > {error} )} - + {filesReceived > 0 && ( - }> - {t('mobileUpload.filesReceived', '{{count}} file(s) received', { count: filesReceived })} + }> + {t("mobileUpload.filesReceived", "{{count}} file(s) received", { count: filesReceived })} )} - + {config?.mobileScannerConvertToPdf !== false ? t( - 'mobileUpload.instructions', - 'Open the camera app on your phone and scan this code. Images will be automatically converted to PDF.' + "mobileUpload.instructions", + "Open the camera app on your phone and scan this code. Images will be automatically converted to PDF.", ) : t( - 'mobileUpload.instructionsNoConvert', - 'Open the camera app on your phone and scan this code. Files will be uploaded through the server.' + "mobileUpload.instructionsNoConvert", + "Open the camera app on your phone and scan this code. Files will be uploaded through the server.", )} @@ -358,9 +360,9 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: size="xs" c="dimmed" style={{ - wordBreak: 'break-all', - textAlign: 'center', - fontFamily: 'monospace', + wordBreak: "break-all", + textAlign: "center", + fontFamily: "monospace", }} > {mobileUrl} diff --git a/frontend/src/core/components/shared/MultiSelectControls.tsx b/frontend/src/core/components/shared/MultiSelectControls.tsx index b6e0b24b9f..856c971130 100644 --- a/frontend/src/core/components/shared/MultiSelectControls.tsx +++ b/frontend/src/core/components/shared/MultiSelectControls.tsx @@ -16,65 +16,43 @@ const MultiSelectControls = ({ onOpenInFileEditor, onOpenInPageEditor, onAddToUpload, - onDeleteAll + onDeleteAll, }: MultiSelectControlsProps) => { const { t } = useTranslation(); if (selectedCount === 0) return null; return ( - + {selectedCount} {t("fileManager.filesSelected", "files selected")} - {onAddToUpload && ( - )} {onOpenInFileEditor && ( - )} {onOpenInPageEditor && ( - )} {onDeleteAll && ( - )} diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx index 8e80b5d771..d35f2a470c 100644 --- a/frontend/src/core/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx @@ -86,33 +86,55 @@ const NavigationWarningModal = () => { zIndex={Z_INDEX_TOAST} > - - - {t("unsavedChanges", "You have unsaved changes to your PDF.")} - - - {t("areYouSure", "Are you sure you want to leave?")} - + + + {t("unsavedChanges", "You have unsaved changes to your PDF.")} + + + {t("areYouSure", "Are you sure you want to leave?")} + {/* Desktop layout: 2 groups side by side */} - - {hasApply && ( - )} {hasExport && ( - )} @@ -121,19 +143,41 @@ const NavigationWarningModal = () => { {/* Mobile layout: centered stack of 4 buttons */} - - {hasApply && ( - )} {hasExport && ( - )} diff --git a/frontend/src/core/components/shared/ObscuredOverlay.tsx b/frontend/src/core/components/shared/ObscuredOverlay.tsx index 2329d624dc..592a79befe 100644 --- a/frontend/src/core/components/shared/ObscuredOverlay.tsx +++ b/frontend/src/core/components/shared/ObscuredOverlay.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import styles from '@app/components/shared/ObscuredOverlay/ObscuredOverlay.module.css'; +import React from "react"; +import styles from "@app/components/shared/ObscuredOverlay/ObscuredOverlay.module.css"; type ObscuredOverlayProps = { obscured: boolean; @@ -30,11 +30,7 @@ export default function ObscuredOverlay({ }} >
- {overlayMessage && ( -
- {overlayMessage} -
- )} + {overlayMessage &&
{overlayMessage}
} {buttonText && onButtonClick && (
); } - - diff --git a/frontend/src/core/components/shared/PageEditorFileDropdown.tsx b/frontend/src/core/components/shared/PageEditorFileDropdown.tsx index 11f9742c2d..2d850d7177 100644 --- a/frontend/src/core/components/shared/PageEditorFileDropdown.tsx +++ b/frontend/src/core/components/shared/PageEditorFileDropdown.tsx @@ -1,16 +1,16 @@ -import React from 'react'; -import { Menu, Loader, Group, Text, Checkbox } from '@mantine/core'; -import { LocalIcon } from '@app/components/shared/LocalIcon'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; -import AddIcon from '@mui/icons-material/Add'; -import FitText from '@app/components/shared/FitText'; -import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { useFileItemDragDrop } from '@app/components/shared/pageEditor/useFileItemDragDrop'; +import React from "react"; +import { Menu, Loader, Group, Text, Checkbox } from "@mantine/core"; +import { LocalIcon } from "@app/components/shared/LocalIcon"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import AddIcon from "@mui/icons-material/Add"; +import FitText from "@app/components/shared/FitText"; +import { getFileColorWithOpacity } from "@app/components/pageEditor/fileColors"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { useFileItemDragDrop } from "@app/components/shared/pageEditor/useFileItemDragDrop"; -import { FileId } from '@app/types/file'; +import { FileId } from "@app/types/file"; // Local interface for PageEditor file display interface PageEditorFile { @@ -28,50 +28,36 @@ interface FileMenuItemProps { onReorder: (fromIndex: number, toIndex: number) => void; } -const FileMenuItem: React.FC = ({ - file, - index, - colorIndex, - onToggleSelection, - onReorder, -}) => { - const { - itemRef, - isDragging, - isDragOver, - dropPosition, - movedRef, - onPointerDown, - onPointerMove, - onPointerUp, - } = useFileItemDragDrop({ - fileId: file.fileId, - index, - onReorder, - }); +const FileMenuItem: React.FC = ({ file, index, colorIndex, onToggleSelection, onReorder }) => { + const { itemRef, isDragging, isDragOver, dropPosition, movedRef, onPointerDown, onPointerMove, onPointerUp } = + useFileItemDragDrop({ + fileId: file.fileId, + index, + onReorder, + }); - const itemName = file?.name || 'Untitled'; + const itemName = file?.name || "Untitled"; const fileColorBorder = getFileColorWithOpacity(colorIndex, 1); const fileColorBorderHover = getFileColorWithOpacity(colorIndex, 1.0); return (
{/* Drop indicator line */} {isDragOver && (
@@ -87,34 +73,36 @@ const FileMenuItem: React.FC = ({ onToggleSelection(file.fileId); }} style={{ - padding: '0.75rem 0.75rem', - cursor: isDragging ? 'grabbing' : 'grab', - backgroundColor: file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent', + padding: "0.75rem 0.75rem", + cursor: isDragging ? "grabbing" : "grab", + backgroundColor: file.isSelected ? "rgba(0, 0, 0, 0.05)" : "transparent", borderLeft: `6px solid ${fileColorBorder}`, opacity: isDragging ? 0.5 : 1, - transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease', - userSelect: 'none', + transition: "opacity 0.2s ease-in-out, background-color 0.15s ease", + userSelect: "none", }} onMouseEnter={(e) => { if (!isDragging) { - (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)'; + (e.currentTarget as HTMLDivElement).style.backgroundColor = "rgba(0, 0, 0, 0.05)"; (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover; } }} onMouseLeave={(e) => { if (!isDragging) { - (e.currentTarget as HTMLDivElement).style.backgroundColor = file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent'; + (e.currentTarget as HTMLDivElement).style.backgroundColor = file.isSelected + ? "rgba(0, 0, 0, 0.05)" + : "transparent"; (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder; } }} > - +
@@ -125,7 +113,7 @@ const FileMenuItem: React.FC = ({ onClick={(e) => e.stopPropagation()} size="sm" /> -
+
@@ -167,24 +155,29 @@ export const PageEditorFileDropdown: React.FC = ({ return ( -
+
{switchingTo === "pageEditor" ? ( ) : ( )} - {selectedCount}/{totalCount} files selected + + {selectedCount}/{totalCount} files selected +
- + {files.map((file, index) => { const colorIndex = fileColorMap.get(file.fileId as string) ?? 0; @@ -207,23 +200,23 @@ export const PageEditorFileDropdown: React.FC = ({ openFilesModal(); }} style={{ - padding: '0.75rem 0.75rem', - marginTop: '0.5rem', - cursor: 'pointer', - backgroundColor: 'transparent', - borderTop: '1px solid var(--border-subtle)', - transition: 'background-color 0.15s ease', + padding: "0.75rem 0.75rem", + marginTop: "0.5rem", + cursor: "pointer", + backgroundColor: "transparent", + borderTop: "1px solid var(--border-subtle)", + transition: "background-color 0.15s ease", }} onMouseEnter={(e) => { - (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(59, 130, 246, 0.25)'; + (e.currentTarget as HTMLDivElement).style.backgroundColor = "rgba(59, 130, 246, 0.25)"; }} onMouseLeave={(e) => { - (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; + (e.currentTarget as HTMLDivElement).style.backgroundColor = "transparent"; }} > - - - + + + Add File diff --git a/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx b/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx index bf7e642066..c44025bf37 100644 --- a/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx +++ b/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx @@ -1,25 +1,25 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Text } from '@mantine/core'; -import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css'; -import { parseSelectionWithDiagnostics } from '@app/utils/bulkselection/parseSelection'; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Text } from "@mantine/core"; +import classes from "@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css"; +import { parseSelectionWithDiagnostics } from "@app/utils/bulkselection/parseSelection"; interface PageSelectionSyntaxHintProps { input: string; /** Optional known page count; if not provided, a large max is used for syntax-only checks */ maxPages?: number; /** panel = full bulk panel style, compact = inline tool style */ - variant?: 'panel' | 'compact'; + variant?: "panel" | "compact"; } const FALLBACK_MAX_PAGES = 100000; // large upper bound for syntax validation without a document -const PageSelectionSyntaxHint = ({ input, maxPages, variant = 'panel' }: PageSelectionSyntaxHintProps) => { +const PageSelectionSyntaxHint = ({ input, maxPages, variant = "panel" }: PageSelectionSyntaxHintProps) => { const [syntaxError, setSyntaxError] = useState(null); const { t } = useTranslation(); useEffect(() => { - const text = (input || '').trim(); + const text = (input || "").trim(); if (!text) { setSyntaxError(null); return; @@ -27,21 +27,23 @@ const PageSelectionSyntaxHint = ({ input, maxPages, variant = 'panel' }: PageSel try { const { warning } = parseSelectionWithDiagnostics(text, maxPages && maxPages > 0 ? maxPages : FALLBACK_MAX_PAGES); - setSyntaxError(warning ? t('bulkSelection.syntaxError', 'There is a syntax issue. See Page Selection tips for help.') : null); + setSyntaxError( + warning ? t("bulkSelection.syntaxError", "There is a syntax issue. See Page Selection tips for help.") : null, + ); } catch { - setSyntaxError(t('bulkSelection.syntaxError', 'There is a syntax issue. See Page Selection tips for help.')); + setSyntaxError(t("bulkSelection.syntaxError", "There is a syntax issue. See Page Selection tips for help.")); } }, [input, maxPages]); if (!syntaxError) return null; return ( -
- {syntaxError} +
+ + {syntaxError} +
); }; export default PageSelectionSyntaxHint; - - diff --git a/frontend/src/core/components/shared/PrivateContent.tsx b/frontend/src/core/components/shared/PrivateContent.tsx index 3ed11bfc6d..a2b048e95c 100644 --- a/frontend/src/core/components/shared/PrivateContent.tsx +++ b/frontend/src/core/components/shared/PrivateContent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface PrivateContentProps extends React.HTMLAttributes { children: React.ReactNode; @@ -23,14 +23,9 @@ interface PrivateContentProps extends React.HTMLAttributes { * preview * */ -export const PrivateContent: React.FC = ({ - children, - className = '', - style, - ...props -}) => { - const combinedClassName = `ph-no-capture${className ? ` ${className}` : ''}`; - const combinedStyle = { display: 'contents' as const, ...style }; +export const PrivateContent: React.FC = ({ children, className = "", style, ...props }) => { + const combinedClassName = `ph-no-capture${className ? ` ${className}` : ""}`; + const combinedStyle = { display: "contents" as const, ...style }; return ( diff --git a/frontend/src/core/components/shared/QuickAccessBar.tsx b/frontend/src/core/components/shared/QuickAccessBar.tsx index 726ed19c43..db57bed92e 100644 --- a/frontend/src/core/components/shared/QuickAccessBar.tsx +++ b/frontend/src/core/components/shared/QuickAccessBar.tsx @@ -1,49 +1,52 @@ import React, { useState, useRef, forwardRef, useEffect, useMemo, useCallback } from "react"; -import { createPortal } from 'react-dom'; +import { createPortal } from "react-dom"; import { Stack, Divider, Menu, Indicator } from "@mantine/core"; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useLocation } from 'react-router-dom'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import SignPopout, { SIGN_REQUEST_WORKBENCH_TYPE, SESSION_DETAIL_WORKBENCH_TYPE } from '@app/components/shared/signing/SignPopout'; +import { useTranslation } from "react-i18next"; +import { useNavigate, useLocation } from "react-router-dom"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import SignPopout, { + SIGN_REQUEST_WORKBENCH_TYPE, + SESSION_DETAIL_WORKBENCH_TYPE, +} from "@app/components/shared/signing/SignPopout"; import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useFileSelection, useFileState } from '@app/contexts/file/fileHooks'; -import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; -import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation'; -import { handleUnlessSpecialClick } from '@app/utils/clickHandlers'; -import { ButtonConfig } from '@app/types/sidebar'; -import '@app/components/shared/quickAccessBar/QuickAccessBar.css'; -import { Tooltip } from '@app/components/shared/Tooltip'; -import AllToolsNavButton from '@app/components/shared/AllToolsNavButton'; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useFileSelection, useFileState } from "@app/contexts/file/fileHooks"; +import { useNavigationState, useNavigationActions } from "@app/contexts/NavigationContext"; +import { useSidebarNavigation } from "@app/hooks/useSidebarNavigation"; +import { handleUnlessSpecialClick } from "@app/utils/clickHandlers"; +import { ButtonConfig } from "@app/types/sidebar"; +import "@app/components/shared/quickAccessBar/QuickAccessBar.css"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import AllToolsNavButton from "@app/components/shared/AllToolsNavButton"; import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolButton"; -import AppConfigModal from '@app/components/shared/AppConfigModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { useGroupSigningEnabled } from '@app/hooks/useGroupSigningEnabled'; -import { useSharingEnabled } from '@app/hooks/useSharingEnabled'; +import AppConfigModal from "@app/components/shared/AppConfigModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useGroupSigningEnabled } from "@app/hooks/useGroupSigningEnabled"; +import { useSharingEnabled } from "@app/hooks/useSharingEnabled"; import { useLicenseAlert } from "@app/hooks/useLicenseAlert"; -import { requestStartTour } from '@app/constants/events'; -import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccessButton'; -import { useToursTooltip } from '@app/components/shared/quickAccessBar/useToursTooltip'; -import ShareManagementModal from '@app/components/shared/ShareManagementModal'; -import apiClient from '@app/services/apiClient'; -import { absoluteWithBasePath } from '@app/constants/app'; -import { alert } from '@app/components/toast'; -import { uploadHistoryChain } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import type { SignRequestSummary } from '@app/types/signingSession'; +import { requestStartTour } from "@app/constants/events"; +import QuickAccessButton from "@app/components/shared/quickAccessBar/QuickAccessButton"; +import { useToursTooltip } from "@app/components/shared/quickAccessBar/useToursTooltip"; +import ShareManagementModal from "@app/components/shared/ShareManagementModal"; +import apiClient from "@app/services/apiClient"; +import { absoluteWithBasePath } from "@app/constants/app"; +import { alert } from "@app/components/toast"; +import { uploadHistoryChain } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { SignRequestSummary } from "@app/types/signingSession"; import { isNavButtonActive, getNavButtonStyle, getActiveNavButton, -} from '@app/components/shared/quickAccessBar/QuickAccessBar'; -import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; -import { QuickAccessBarFooterExtensions } from '@app/components/quickAccessBar/QuickAccessBarFooterExtensions'; -import { useConfigButtonIcon } from '@app/hooks/useConfigButtonIcon'; +} from "@app/components/shared/quickAccessBar/QuickAccessBar"; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex"; +import { QuickAccessBarFooterExtensions } from "@app/components/quickAccessBar/QuickAccessBarFooterExtensions"; +import { useConfigButtonIcon } from "@app/hooks/useConfigButtonIcon"; const QuickAccessBar = forwardRef((_, ref) => { const { t } = useTranslation(); @@ -59,7 +62,7 @@ const QuickAccessBar = forwardRef((_, ref) => { toolRegistry, readerMode, resetTool, - toolAvailability + toolAvailability, } = useToolWorkflow(); const { selectedFiles, selectedFileIds } = useFileSelection(); const { state, selectors } = useFileState(); @@ -70,7 +73,7 @@ const QuickAccessBar = forwardRef((_, ref) => { const { config } = useAppConfig(); const licenseAlert = useLicenseAlert(); const [configModalOpen, setConfigModalOpen] = useState(false); - const [activeButton, setActiveButton] = useState('tools'); + const [activeButton, setActiveButton] = useState("tools"); const [accessMenuOpen, setAccessMenuOpen] = useState(false); const [accessInviteOpen, setAccessInviteOpen] = useState(false); const [selectedAccessFileId, setSelectedAccessFileId] = useState(null); @@ -82,11 +85,10 @@ const QuickAccessBar = forwardRef((_, ref) => { const { sharingEnabled, shareLinksEnabled } = useSharingEnabled(); const groupSigningEnabled = useGroupSigningEnabled(); const isSignWorkbenchActive = - currentWorkbench === SIGN_REQUEST_WORKBENCH_TYPE || - currentWorkbench === SESSION_DETAIL_WORKBENCH_TYPE; - const [inviteRows, setInviteRows] = useState>([ - { id: Date.now(), email: '', role: 'editor' }, - ]); + currentWorkbench === SIGN_REQUEST_WORKBENCH_TYPE || currentWorkbench === SESSION_DETAIL_WORKBENCH_TYPE; + const [inviteRows, setInviteRows] = useState< + Array<{ id: number; email: string; role: "editor" | "commenter" | "viewer"; error?: string }> + >([{ id: Date.now(), email: "", role: "editor" }]); const [isInviting, setIsInviting] = useState(false); // Sign button state @@ -99,12 +101,12 @@ const QuickAccessBar = forwardRef((_, ref) => { if (!groupSigningEnabled) return; const fetchCount = async () => { try { - const response = await apiClient.get('/api/v1/security/cert-sign/sign-requests'); - const pending = response.data.filter( - r => r.myStatus !== 'SIGNED' && r.myStatus !== 'DECLINED' - ).length; + const response = await apiClient.get("/api/v1/security/cert-sign/sign-requests"); + const pending = response.data.filter((r) => r.myStatus !== "SIGNED" && r.myStatus !== "DECLINED").length; setPendingSignCount(pending); - } catch { /* silent — avoid noisy background error toasts */ } + } catch { + /* silent — avoid noisy background error toasts */ + } }; fetchCount(); const interval = setInterval(fetchCount, 60000); @@ -116,12 +118,12 @@ const QuickAccessBar = forwardRef((_, ref) => { if (!signMenuOpen && groupSigningEnabled) { const timeout = setTimeout(async () => { try { - const response = await apiClient.get('/api/v1/security/cert-sign/sign-requests'); - const pending = response.data.filter( - r => r.myStatus !== 'SIGNED' && r.myStatus !== 'DECLINED' - ).length; + const response = await apiClient.get("/api/v1/security/cert-sign/sign-requests"); + const pending = response.data.filter((r) => r.myStatus !== "SIGNED" && r.myStatus !== "DECLINED").length; setPendingSignCount(pending); - } catch { /* silent */ } + } catch { + /* silent */ + } }, 500); return () => clearTimeout(timeout); } @@ -129,23 +131,16 @@ const QuickAccessBar = forwardRef((_, ref) => { const configButtonIcon = useConfigButtonIcon(); - const { - tooltipOpen, - manualCloseOnly, - showCloseButton, - toursMenuOpen, - setToursMenuOpen, - handleTooltipOpenChange, - } = useToursTooltip(); + const { tooltipOpen, manualCloseOnly, showCloseButton, toursMenuOpen, setToursMenuOpen, handleTooltipOpenChange } = + useToursTooltip(); - const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; + const isRTL = typeof document !== "undefined" && document.documentElement.dir === "rtl"; const hasSelectedFiles = selectedFiles.length > 0; const selectedFileStubs = useMemo( () => selectedFileIds.map((id) => selectors.getStirlingFileStub(id)).filter((x): x is StirlingFileStub => Boolean(x)), - [selectedFileIds, selectors, state.files.byId] + [selectedFileIds, selectors, state.files.byId], ); - const selectedAccessFileStub = - selectedFileStubs.find((file) => file.id === selectedAccessFileId) || selectedFileStubs[0]; + const selectedAccessFileStub = selectedFileStubs.find((file) => file.id === selectedAccessFileId) || selectedFileStubs[0]; useEffect(() => { if (!hasSelectedFiles) { setAccessMenuOpen(false); @@ -159,7 +154,7 @@ const QuickAccessBar = forwardRef((_, ref) => { }, [hasSelectedFiles, selectedAccessFileId, selectedFiles]); const resetInviteRows = useCallback(() => { - setInviteRows([{ id: Date.now(), email: '', role: 'editor' }]); + setInviteRows([{ id: Date.now(), email: "", role: "editor" }]); }, []); useEffect(() => { @@ -176,11 +171,11 @@ const QuickAccessBar = forwardRef((_, ref) => { setAccessPopoverPosition({ top, left }); }; updatePosition(); - window.addEventListener('resize', updatePosition); - window.addEventListener('scroll', updatePosition, true); + window.addEventListener("resize", updatePosition); + window.addEventListener("scroll", updatePosition, true); return () => { - window.removeEventListener('resize', updatePosition); - window.removeEventListener('scroll', updatePosition, true); + window.removeEventListener("resize", updatePosition); + window.removeEventListener("scroll", updatePosition, true); }; }, [accessMenuOpen, isRTL, resetInviteRows]); @@ -192,77 +187,77 @@ const QuickAccessBar = forwardRef((_, ref) => { if (accessButtonRef.current?.contains(target)) return; // Check if click is inside a Mantine dropdown - const mantineDropdown = (target as Element).closest?.('.mantine-Combobox-dropdown, .mantine-Popover-dropdown'); + const mantineDropdown = (target as Element).closest?.(".mantine-Combobox-dropdown, .mantine-Popover-dropdown"); if (mantineDropdown) return; setAccessMenuOpen(false); }; const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { + if (event.key === "Escape") { setAccessMenuOpen(false); } }; - document.addEventListener('mousedown', handleOutside); - document.addEventListener('keydown', handleEscape); + document.addEventListener("mousedown", handleOutside); + document.addEventListener("keydown", handleEscape); return () => { - document.removeEventListener('mousedown', handleOutside); - document.removeEventListener('keydown', handleEscape); + document.removeEventListener("mousedown", handleOutside); + document.removeEventListener("keydown", handleEscape); }; }, [accessMenuOpen]); const shareBaseUrl = useMemo(() => { - const frontendUrl = (config?.frontendUrl || '').trim(); + const frontendUrl = (config?.frontendUrl || "").trim(); if (frontendUrl) { try { const parsed = new URL(frontendUrl); - if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { - const normalized = frontendUrl.endsWith('/') ? frontendUrl.slice(0, -1) : frontendUrl; + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + const normalized = frontendUrl.endsWith("/") ? frontendUrl.slice(0, -1) : frontendUrl; return `${normalized}/share/`; } } catch { // invalid URL — fall through to default } } - return absoluteWithBasePath('/share/'); + return absoluteWithBasePath("/share/"); }, [config?.frontendUrl]); - const ensureStoredFile = useCallback(async (fileStub: StirlingFileStub): Promise => { - const localUpdatedAt = fileStub.createdAt ?? fileStub.lastModified ?? 0; - const isUpToDate = - Boolean(fileStub.remoteStorageId) && - Boolean(fileStub.remoteStorageUpdatedAt) && - (fileStub.remoteStorageUpdatedAt as number) >= localUpdatedAt; - if (isUpToDate && fileStub.remoteStorageId) { - return fileStub.remoteStorageId as number; - } - const originalFileId = (fileStub.originalFileId || fileStub.id) as FileId; - const remoteId = fileStub.remoteStorageId as number | undefined; - const { remoteId: storedId, updatedAt, chain } = await uploadHistoryChain( - originalFileId, - remoteId - ); - for (const stub of chain) { - actions.updateStirlingFileStub(stub.id, { - remoteStorageId: storedId, - remoteStorageUpdatedAt: updatedAt, - remoteOwnedByCurrentUser: true, - remoteSharedViaLink: false, - }); - await fileStorage.updateFileMetadata(stub.id, { - remoteStorageId: storedId, - remoteStorageUpdatedAt: updatedAt, - remoteOwnedByCurrentUser: true, - remoteSharedViaLink: false, - }); - } - return storedId; - }, [actions]); + const ensureStoredFile = useCallback( + async (fileStub: StirlingFileStub): Promise => { + const localUpdatedAt = fileStub.createdAt ?? fileStub.lastModified ?? 0; + const isUpToDate = + Boolean(fileStub.remoteStorageId) && + Boolean(fileStub.remoteStorageUpdatedAt) && + (fileStub.remoteStorageUpdatedAt as number) >= localUpdatedAt; + if (isUpToDate && fileStub.remoteStorageId) { + return fileStub.remoteStorageId as number; + } + const originalFileId = (fileStub.originalFileId || fileStub.id) as FileId; + const remoteId = fileStub.remoteStorageId as number | undefined; + const { remoteId: storedId, updatedAt, chain } = await uploadHistoryChain(originalFileId, remoteId); + for (const stub of chain) { + actions.updateStirlingFileStub(stub.id, { + remoteStorageId: storedId, + remoteStorageUpdatedAt: updatedAt, + remoteOwnedByCurrentUser: true, + remoteSharedViaLink: false, + }); + await fileStorage.updateFileMetadata(stub.id, { + remoteStorageId: storedId, + remoteStorageUpdatedAt: updatedAt, + remoteOwnedByCurrentUser: true, + remoteSharedViaLink: false, + }); + } + return storedId; + }, + [actions], + ); const openShareManage = useCallback(async () => { if (!sharingEnabled) { alert({ - alertType: 'warning', - title: t('storageShare.sharingDisabled', 'Sharing is disabled.'), + alertType: "warning", + title: t("storageShare.sharingDisabled", "Sharing is disabled."), expandable: false, durationMs: 2500, }); @@ -270,8 +265,8 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedFileStubs.length > 1) { alert({ - alertType: 'warning', - title: t('storageShare.selectSingleFile', 'Select a single file to manage sharing.'), + alertType: "warning", + title: t("storageShare.selectSingleFile", "Select a single file to manage sharing."), expandable: false, durationMs: 2500, }); @@ -279,8 +274,8 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedAccessFileStub?.remoteOwnedByCurrentUser === false) { alert({ - alertType: 'warning', - title: t('storageShare.ownerOnly', 'Only the owner can manage sharing.'), + alertType: "warning", + title: t("storageShare.ownerOnly", "Only the owner can manage sharing."), expandable: false, durationMs: 2500, }); @@ -293,10 +288,10 @@ const QuickAccessBar = forwardRef((_, ref) => { setAccessMenuOpen(false); setShareManageOpen(true); } catch (error) { - console.error('Failed to upload file for sharing:', error); + console.error("Failed to upload file for sharing:", error); alert({ - alertType: 'warning', - title: t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.'), + alertType: "warning", + title: t("storageUpload.failure", "Upload failed. Please check your login and storage settings."), expandable: false, durationMs: 3000, }); @@ -304,22 +299,20 @@ const QuickAccessBar = forwardRef((_, ref) => { }, [ensureStoredFile, selectedAccessFileStub, selectedFileStubs.length, sharingEnabled, t]); const handleInviteRowChange = useCallback( - (id: number, updates: Partial<{ email: string; role: 'editor' | 'commenter' | 'viewer'; error?: string }>) => { + (id: number, updates: Partial<{ email: string; role: "editor" | "commenter" | "viewer"; error?: string }>) => { setInviteRows((prev) => prev.map((row) => { if (row.id !== id) return row; - const nextError = Object.prototype.hasOwnProperty.call(updates, 'error') - ? updates.error - : row.error; + const nextError = Object.prototype.hasOwnProperty.call(updates, "error") ? updates.error : row.error; return { ...row, ...updates, error: nextError }; - }) + }), ); }, - [] + [], ); const handleAddInviteRow = useCallback(() => { - setInviteRows((prev) => [...prev, { id: Date.now(), email: '', role: 'editor' }]); + setInviteRows((prev) => [...prev, { id: Date.now(), email: "", role: "editor" }]); }, []); const handleRemoveInviteRow = useCallback((id: number) => { @@ -330,8 +323,8 @@ const QuickAccessBar = forwardRef((_, ref) => { if (!selectedAccessFileStub) return; if (selectedAccessFileStub.remoteOwnedByCurrentUser === false) { alert({ - alertType: 'warning', - title: t('storageShare.ownerOnly', 'Only the owner can manage sharing.'), + alertType: "warning", + title: t("storageShare.ownerOnly", "Only the owner can manage sharing."), expandable: false, durationMs: 2500, }); @@ -341,7 +334,7 @@ const QuickAccessBar = forwardRef((_, ref) => { const trimmed = row.email.trim(); let error: string | undefined; if (!trimmed) { - error = t('storageShare.invalidUsername', 'Enter a valid username or email address.'); + error = t("storageShare.invalidUsername", "Enter a valid username or email address."); } return { ...row, email: trimmed, error }; }); @@ -359,18 +352,18 @@ const QuickAccessBar = forwardRef((_, ref) => { }); } alert({ - alertType: 'success', - title: t('storageShare.userAdded', 'User added to shared list.'), + alertType: "success", + title: t("storageShare.userAdded", "User added to shared list."), expandable: false, durationMs: 2500, }); setAccessInviteOpen(false); resetInviteRows(); } catch (error) { - console.error('Failed to send invite:', error); + console.error("Failed to send invite:", error); alert({ - alertType: 'warning', - title: t('storageShare.userAddFailed', 'Unable to share with that user.'), + alertType: "warning", + title: t("storageShare.userAddFailed", "Unable to share with that user."), expandable: false, durationMs: 3000, }); @@ -383,8 +376,8 @@ const QuickAccessBar = forwardRef((_, ref) => { if (!selectedAccessFileStub) return; if (!shareLinksEnabled) { alert({ - alertType: 'warning', - title: t('storageShare.linksDisabled', 'Share links are disabled.'), + alertType: "warning", + title: t("storageShare.linksDisabled", "Share links are disabled."), expandable: false, durationMs: 2500, }); @@ -392,8 +385,8 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedFileStubs.length > 1) { alert({ - alertType: 'warning', - title: t('storageShare.selectSingleFile', 'Select a single file to copy a link.'), + alertType: "warning", + title: t("storageShare.selectSingleFile", "Select a single file to copy a link."), expandable: false, durationMs: 2500, }); @@ -401,8 +394,8 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedAccessFileStub?.remoteOwnedByCurrentUser === false) { alert({ - alertType: 'warning', - title: t('storageShare.ownerOnly', 'Only the owner can manage sharing.'), + alertType: "warning", + title: t("storageShare.ownerOnly", "Only the owner can manage sharing."), expandable: false, durationMs: 2500, }); @@ -412,10 +405,10 @@ const QuickAccessBar = forwardRef((_, ref) => { try { await ensureStoredFile(selectedAccessFileStub); } catch (error) { - console.error('Failed to upload file for sharing:', error); + console.error("Failed to upload file for sharing:", error); alert({ - alertType: 'warning', - title: t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.'), + alertType: "warning", + title: t("storageUpload.failure", "Upload failed. Please check your login and storage settings."), expandable: false, durationMs: 3000, }); @@ -424,15 +417,14 @@ const QuickAccessBar = forwardRef((_, ref) => { } try { const storedId = await ensureStoredFile(selectedAccessFileStub); - const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>( - `/api/v1/storage/files/${storedId}`, - { suppressErrorToast: true } - ); + const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>(`/api/v1/storage/files/${storedId}`, { + suppressErrorToast: true, + }); const links = response.data?.shareLinks ?? []; let token = links[links.length - 1]?.token; if (!token) { const shareResponse = await apiClient.post(`/api/v1/storage/files/${storedId}/shares/links`, { - accessRole: 'editor', + accessRole: "editor", }); token = shareResponse.data?.token; if (token) { @@ -442,8 +434,8 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (!token) { alert({ - alertType: 'warning', - title: t('storageShare.failure', 'Unable to generate a share link. Please try again.'), + alertType: "warning", + title: t("storageShare.failure", "Unable to generate a share link. Please try again."), expandable: false, durationMs: 2500, }); @@ -451,26 +443,25 @@ const QuickAccessBar = forwardRef((_, ref) => { } await navigator.clipboard.writeText(`${shareBaseUrl}${token}`); alert({ - alertType: 'success', - title: t('storageShare.copied', 'Link copied to clipboard'), + alertType: "success", + title: t("storageShare.copied", "Link copied to clipboard"), expandable: false, durationMs: 2000, }); } catch (error) { - console.error('Failed to copy share link:', error); + console.error("Failed to copy share link:", error); alert({ - alertType: 'warning', - title: t('storageShare.copyFailed', 'Copy failed'), + alertType: "warning", + title: t("storageShare.copyFailed", "Copy failed"), expandable: false, durationMs: 2500, }); } }; - // Open modal if URL is at /settings/* useEffect(() => { - const isSettings = location.pathname.startsWith('/settings'); + const isSettings = location.pathname.startsWith("/settings"); setConfigModalOpen(isSettings); }, [location.pathname]); @@ -485,12 +476,13 @@ const QuickAccessBar = forwardRef((_, ref) => { // Helper function to render navigation buttons with URL support const renderNavButton = (config: ButtonConfig, index: number, shouldGuardNavigation = false) => { - const isActive = !isSignWorkbenchActive && isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); + const isActive = + !isSignWorkbenchActive && + isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); // Check if this button has URL navigation support - const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate') - ? getToolNavigation(config.id) - : null; + const navProps = + config.type === "navigation" && (config.id === "read" || config.id === "automate") ? getToolNavigation(config.id) : null; const handleClick = (e?: React.MouseEvent) => { // If there are unsaved changes and this button should guard navigation, show warning modal @@ -509,15 +501,17 @@ const QuickAccessBar = forwardRef((_, ref) => { }; const buttonStyle = isSignWorkbenchActive - ? { backgroundColor: 'var(--icon-inactive-bg)', color: 'var(--icon-inactive-color)', border: 'none', borderRadius: '0.5rem' } + ? { + backgroundColor: "var(--icon-inactive-bg)", + color: "var(--icon-inactive-color)", + border: "none", + borderRadius: "0.5rem", + } : getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); // Render navigation button with conditional URL support return ( -
+
((_, ref) => { ariaLabel={config.name} backgroundColor={buttonStyle.backgroundColor} color={buttonStyle.color} - component={navProps ? 'a' : 'button'} + component={navProps ? "a" : "button"} dataTestId={`${config.id}-button`} dataTour={`${config.id}-button`} /> @@ -535,54 +529,58 @@ const QuickAccessBar = forwardRef((_, ref) => { ); }; - const mainButtons: ButtonConfig[] = useMemo(() => [ - { - id: 'read', - name: t("quickAccess.reader", "Reader"), - icon: , - size: 'md' as const, - isRound: false, - type: 'navigation' as const, - onClick: () => { - setActiveButton('read'); - handleReaderToggle(); - } - }, - { - id: 'automate', - name: t("quickAccess.automate", "Automate"), - icon: , - size: 'md' as const, - isRound: false, - type: 'navigation' as const, - onClick: () => { - setActiveButton('automate'); - // If already on automate tool, reset it directly - if (selectedToolKey === 'automate') { - resetTool('automate'); - } else { - handleToolSelect('automate'); - } - } - }, - ].filter(button => { - // Filter out buttons for disabled tools - // 'read' is always available (viewer mode) - if (button.id === 'read') return true; - // Check if tool is actually available (not just present in registry) - const availability = toolAvailability[button.id as keyof typeof toolAvailability]; - return availability?.available !== false; - }), [t, setActiveButton, handleReaderToggle, selectedToolKey, resetTool, handleToolSelect, toolAvailability]); + const mainButtons: ButtonConfig[] = useMemo( + () => + [ + { + id: "read", + name: t("quickAccess.reader", "Reader"), + icon: , + size: "md" as const, + isRound: false, + type: "navigation" as const, + onClick: () => { + setActiveButton("read"); + handleReaderToggle(); + }, + }, + { + id: "automate", + name: t("quickAccess.automate", "Automate"), + icon: , + size: "md" as const, + isRound: false, + type: "navigation" as const, + onClick: () => { + setActiveButton("automate"); + // If already on automate tool, reset it directly + if (selectedToolKey === "automate") { + resetTool("automate"); + } else { + handleToolSelect("automate"); + } + }, + }, + ].filter((button) => { + // Filter out buttons for disabled tools + // 'read' is always available (viewer mode) + if (button.id === "read") return true; + // Check if tool is actually available (not just present in registry) + const availability = toolAvailability[button.id as keyof typeof toolAvailability]; + return availability?.available !== false; + }), + [t, setActiveButton, handleReaderToggle, selectedToolKey, resetTool, handleToolSelect, toolAvailability], + ); const middleButtons: ButtonConfig[] = [ { - id: 'files', + id: "files", name: t("quickAccess.files", "Files"), icon: , isRound: true, - size: 'md', - type: 'modal', - onClick: handleFilesButtonClick + size: "md", + type: "modal", + onClick: handleFilesButtonClick, }, ]; //TODO: Activity @@ -598,51 +596,50 @@ const QuickAccessBar = forwardRef((_, ref) => { // Determine if settings button should be hidden // Hide when login is disabled AND showSettingsWhenNoLogin is false - const shouldHideSettingsButton = - config?.enableLogin === false && - config?.showSettingsWhenNoLogin === false; + const shouldHideSettingsButton = config?.enableLogin === false && config?.showSettingsWhenNoLogin === false; const bottomButtons: ButtonConfig[] = [ { - id: 'help', + id: "help", name: t("quickAccess.tours", "Tours"), icon: , isRound: true, - size: 'md', - type: 'action', + size: "md", + type: "action", onClick: () => { // This will be overridden by the wrapper logic }, }, - ...(shouldHideSettingsButton ? [] : [{ - id: 'config', - name: t("quickAccess.settings", "Settings"), - icon: configButtonIcon ?? , - size: 'md' as const, - type: 'modal' as const, - onClick: () => { - navigate('/settings/overview'); - setConfigModalOpen(true); - } - } as ButtonConfig]) + ...(shouldHideSettingsButton + ? [] + : [ + { + id: "config", + name: t("quickAccess.settings", "Settings"), + icon: configButtonIcon ?? , + size: "md" as const, + type: "modal" as const, + onClick: () => { + navigate("/settings/overview"); + setConfigModalOpen(true); + }, + } as ButtonConfig, + ]), ]; - return (
{/* Fixed header outside scrollable area */}
-
- {/* Scrollable content area */}
((_, ref) => { {mainButtons.map((config, index) => ( - {renderNavButton(config, index, config.id === 'read' || config.id === 'automate')} + {renderNavButton(config, index, config.id === "read" || config.id === "automate")} ))} @@ -665,57 +662,45 @@ const QuickAccessBar = forwardRef((_, ref) => { {/* Middle section */} {middleButtons.length > 0 && ( <> - + {middleButtons.map((config, index) => ( - - {renderNavButton(config, index)} - + {renderNavButton(config, index)} ))} {hasSelectedFiles && sharingEnabled && (
} - label={t('quickAccess.access', 'Access')} + label={t("quickAccess.access", "Access")} isActive={!isSignWorkbenchActive && accessMenuOpen} onClick={() => { setAccessMenuOpen((prev) => !prev); }} - ariaLabel={t('quickAccess.access', 'Access')} + ariaLabel={t("quickAccess.access", "Access")} dataTestId="access-button" />
)} {groupSigningEnabled && ( -
+
{pendingSignCount > 0 ? ( - + } - label={t('quickAccess.sign', 'Sign')} + label={t("quickAccess.sign", "Sign")} isActive={signMenuOpen || isSignWorkbenchActive} onClick={() => setSignMenuOpen((prev) => !prev)} - ariaLabel={t('quickAccess.sign', 'Sign')} + ariaLabel={t("quickAccess.sign", "Sign")} dataTestId="sign-button" /> ) : ( } - label={t('quickAccess.sign', 'Sign')} + label={t("quickAccess.sign", "Sign")} isActive={signMenuOpen || isSignWorkbenchActive} onClick={() => setSignMenuOpen((prev) => !prev)} - ariaLabel={t('quickAccess.sign', 'Sign')} + ariaLabel={t("quickAccess.sign", "Sign")} dataTestId="sign-button" /> )} @@ -734,39 +719,46 @@ const QuickAccessBar = forwardRef((_, ref) => { {bottomButtons.map((buttonConfig, index) => { // Handle help button with menu or direct action - if (buttonConfig.id === 'help') { + if (buttonConfig.id === "help") { const isAdmin = config?.isAdmin === true; const toursTooltipContent = isAdmin - ? t('quickAccess.toursTooltip.admin', 'Watch walkthroughs here: Tools tour, New V2 layout tour, and the Admin tour.') - : t('quickAccess.toursTooltip.user', 'Watch walkthroughs here: Tools tour and the New V2 layout tour.'); + ? t( + "quickAccess.toursTooltip.admin", + "Watch walkthroughs here: Tools tour, New V2 layout tour, and the Admin tour.", + ) + : t("quickAccess.toursTooltip.user", "Watch walkthroughs here: Tools tour and the New V2 layout tour."); const tourItems = [ { - key: 'whatsnew', + key: "whatsnew", icon: , title: t("quickAccess.helpMenu.whatsNewTour", "See what's new in V2"), description: t("quickAccess.helpMenu.whatsNewTourDesc", "Tour the updated layout"), - onClick: () => requestStartTour('whatsnew'), + onClick: () => requestStartTour("whatsnew"), }, { - key: 'tools', + key: "tools", icon: , title: t("quickAccess.helpMenu.toolsTour", "Tools Tour"), description: t("quickAccess.helpMenu.toolsTourDesc", "Learn what the tools can do"), - onClick: () => requestStartTour('tools'), + onClick: () => requestStartTour("tools"), }, - ...(isAdmin ? [{ - key: 'admin', - icon: , - title: t("quickAccess.helpMenu.adminTour", "Admin Tour"), - description: t("quickAccess.helpMenu.adminTourDesc", "Explore admin settings & features"), - onClick: () => requestStartTour('admin'), - }] : []), + ...(isAdmin + ? [ + { + key: "admin", + icon: , + title: t("quickAccess.helpMenu.adminTour", "Admin Tour"), + description: t("quickAccess.helpMenu.adminTourDesc", "Explore admin settings & features"), + onClick: () => requestStartTour("admin"), + }, + ] + : []), ]; const helpButtonNode = (
((_, ref) => { {tourItems.map((item) => ( - +
-
- {item.title} -
-
- {item.description} -
+
{item.title}
+
{item.description}
))} @@ -819,20 +803,12 @@ const QuickAccessBar = forwardRef((_, ref) => { const buttonNode = renderNavButton(buttonConfig, index); const shouldShowSettingsBadge = - buttonConfig.id === 'config' && - licenseAlert.active && - licenseAlert.audience === 'admin'; + buttonConfig.id === "config" && licenseAlert.active && licenseAlert.audience === "admin"; return ( {shouldShowSettingsBadge ? ( - + {buttonNode} ) : ( @@ -845,10 +821,7 @@ const QuickAccessBar = forwardRef((_, ref) => {
- setConfigModalOpen(false)} - /> + setConfigModalOpen(false)} /> {selectedAccessFileStub && ( ((_, ref) => { file={selectedAccessFileStub} /> )} - {hasSelectedFiles && typeof document !== 'undefined' && createPortal( -
-
-
- -
- {accessInviteOpen - ? t('quickAccess.accessInviteTitle', 'Invite People') - : t('quickAccess.accessTitle', 'Document Access')} -
-
- {!accessInviteOpen && ( + {hasSelectedFiles && + typeof document !== "undefined" && + createPortal( +
+
+
+ +
+ {accessInviteOpen + ? t("quickAccess.accessInviteTitle", "Invite People") + : t("quickAccess.accessTitle", "Document Access")} +
+
+ {!accessInviteOpen && ( + + )} - )} - -
-
- -
-
-
-
- {t('quickAccess.accessFileLabel', 'File')} -
- -
- -
- -
-
- {t('quickAccess.accessGeneral', 'General Access')} -
-
-
- -
-
-
- {t('quickAccess.accessRestricted', 'Restricted')} -
-
- {t('quickAccess.accessRestrictedHint', 'Only people with access can open')} -
-
-
-
- -
- -
-
- {t('quickAccess.accessPeople', 'People with access')} -
-
-
- {(selectedAccessFileStub?.remoteOwnerUsername || 'You').slice(0, 2).toUpperCase()} -
-
-
- {selectedAccessFileStub?.remoteOwnerUsername || t('quickAccess.accessYou', 'You')} -
-
- {selectedAccessFileStub?.name ?? t('quickAccess.accessSelectedFile', 'Selected file')} -
-
- - {t('quickAccess.accessOwner', 'Owner')} - -
-
-
-
- {t('quickAccess.accessInviteTitle', 'Invite People')} +
+
+
+
{t("quickAccess.accessFileLabel", "File")}
+ +
+ +
+ +
+
{t("quickAccess.accessGeneral", "General Access")}
+
+
+ +
+
+
{t("quickAccess.accessRestricted", "Restricted")}
+
+ {t("quickAccess.accessRestrictedHint", "Only people with access can open")} +
+
+
+
+ +
+ +
+
{t("quickAccess.accessPeople", "People with access")}
+
+
+ {(selectedAccessFileStub?.remoteOwnerUsername || "You").slice(0, 2).toUpperCase()} +
+
+
+ {selectedAccessFileStub?.remoteOwnerUsername || t("quickAccess.accessYou", "You")} +
+
+ {selectedAccessFileStub?.name ?? t("quickAccess.accessSelectedFile", "Selected file")} +
+
+ {t("quickAccess.accessOwner", "Owner")} +
- {inviteRows.map((row) => ( -
-
- - - handleInviteRowChange(row.id, { email: event.target.value, error: undefined }) - } - /> - {row.error && ( -
{row.error}
- )} -
-
- - handleInviteRowChange(row.id, { email: event.target.value, error: undefined })} + /> + {row.error &&
{row.error}
} +
+
+ + +
+
- -
- ))} - + ))} + +
-
- -
- {accessInviteOpen ? ( - <> - - {shareLinksEnabled && ( - - )} - - ) : ( - <> - {sharingEnabled && ( +
+ {accessInviteOpen ? ( + <> - )} - {shareLinksEnabled && ( - - )} - - )} + {shareLinksEnabled && ( + + )} + + ) : ( + <> + {sharingEnabled && ( + + )} + {shareLinksEnabled && ( + + )} + + )} +
-
-
, - document.body - )} +
, + document.body, + )} {/* Sign Popover */} ((_, ref) => { ); }); -QuickAccessBar.displayName = 'QuickAccessBar'; +QuickAccessBar.displayName = "QuickAccessBar"; export default QuickAccessBar; diff --git a/frontend/src/core/components/shared/RainbowThemeProvider.tsx b/frontend/src/core/components/shared/RainbowThemeProvider.tsx index 992aa79b5c..a3fa5e04af 100644 --- a/frontend/src/core/components/shared/RainbowThemeProvider.tsx +++ b/frontend/src/core/components/shared/RainbowThemeProvider.tsx @@ -1,12 +1,12 @@ -import { createContext, useContext, ReactNode } from 'react'; -import { MantineProvider } from '@mantine/core'; -import { useRainbowTheme } from '@app/hooks/useRainbowTheme'; -import { mantineTheme } from '@app/theme/mantineTheme'; -import rainbowStyles from '@app/styles/rainbow.module.css'; -import { ToastProvider } from '@app/components/toast'; -import ToastRenderer from '@app/components/toast/ToastRenderer'; -import { ToastPortalBinder } from '@app/components/toast'; -import type { ThemeMode } from '@app/constants/theme'; +import { createContext, useContext, ReactNode } from "react"; +import { MantineProvider } from "@mantine/core"; +import { useRainbowTheme } from "@app/hooks/useRainbowTheme"; +import { mantineTheme } from "@app/theme/mantineTheme"; +import rainbowStyles from "@app/styles/rainbow.module.css"; +import { ToastProvider } from "@app/components/toast"; +import ToastRenderer from "@app/components/toast/ToastRenderer"; +import { ToastPortalBinder } from "@app/components/toast"; +import type { ThemeMode } from "@app/constants/theme"; interface RainbowThemeContextType { themeMode: ThemeMode; @@ -22,7 +22,7 @@ const RainbowThemeContext = createContext(null); export function useRainbowThemeContext() { const context = useContext(RainbowThemeContext); if (!context) { - throw new Error('useRainbowThemeContext must be used within RainbowThemeProvider'); + throw new Error("useRainbowThemeContext must be used within RainbowThemeProvider"); } return context; } @@ -35,19 +35,12 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) { const rainbowTheme = useRainbowTheme(); // Determine the Mantine color scheme - const mantineColorScheme = rainbowTheme.themeMode === 'rainbow' ? 'dark' : rainbowTheme.themeMode; + const mantineColorScheme = rainbowTheme.themeMode === "rainbow" ? "dark" : rainbowTheme.themeMode; return ( - -
+ +
{children} diff --git a/frontend/src/core/components/shared/RightRail.tsx b/frontend/src/core/components/shared/RightRail.tsx index 8001b17f73..639a354e73 100644 --- a/frontend/src/core/components/shared/RightRail.tsx +++ b/frontend/src/core/components/shared/RightRail.tsx @@ -1,40 +1,40 @@ -import React, { useCallback, useMemo } from 'react'; -import { ActionIcon, Divider } from '@mantine/core'; -import '@app/components/shared/rightRail/RightRail.css'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useRightRail } from '@app/contexts/RightRailContext'; -import { useFileState, useFileSelection, useFileActions } from '@app/contexts/FileContext'; -import { isStirlingFile } from '@app/types/fileContext'; -import { useNavigationState } from '@app/contexts/NavigationContext'; -import { useTranslation } from 'react-i18next'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; +import React, { useCallback, useMemo } from "react"; +import { ActionIcon, Divider } from "@mantine/core"; +import "@app/components/shared/rightRail/RightRail.css"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useRightRail } from "@app/contexts/RightRailContext"; +import { useFileState, useFileSelection, useFileActions } from "@app/contexts/FileContext"; +import { isStirlingFile } from "@app/types/fileContext"; +import { useNavigationState } from "@app/contexts/NavigationContext"; +import { useTranslation } from "react-i18next"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; -import LanguageSelector from '@app/components/shared/LanguageSelector'; -import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider'; -import { Tooltip } from '@app/components/shared/Tooltip'; -import { ViewerContext } from '@app/contexts/ViewerContext'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; +import LanguageSelector from "@app/components/shared/LanguageSelector"; +import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import { ViewerContext } from "@app/contexts/ViewerContext"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { RightRailFooterExtensions } from "@app/components/rightRail/RightRailFooterExtensions"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; -import { useSidebarContext } from '@app/contexts/SidebarContext'; -import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail'; -import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; -import { downloadFile } from '@app/services/downloadService'; +import { useSidebarContext } from "@app/contexts/SidebarContext"; +import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from "@app/types/rightRail"; +import { useRightRailTooltipSide } from "@app/hooks/useRightRailTooltipSide"; +import { downloadFile } from "@app/services/downloadService"; -const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom']; +const SECTION_ORDER: RightRailSection[] = ["top", "middle", "bottom"]; function renderWithTooltip( node: React.ReactNode, tooltip: React.ReactNode | undefined, - position: 'left' | 'right', - offset: number + position: "left" | "right", + offset: number, ) { if (!tooltip) return node; - const portalTarget = typeof document !== 'undefined' ? document.body : undefined; + const portalTarget = typeof document !== "undefined" ? document.body : undefined; return ( @@ -54,7 +54,7 @@ export default function RightRail() { const { buttons, actions, allButtonsDisabled } = useRightRail(); const { pageEditorFunctions, toolPanelMode, leftPanelView } = useToolWorkflow(); - const disableForFullscreen = toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker'; + const disableForFullscreen = toolPanelMode === "fullscreen" && leftPanelView === "toolPicker"; const { workbench: currentView } = useNavigationState(); @@ -66,24 +66,22 @@ export default function RightRail() { const pageEditorSelectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0; const totalItems = useMemo(() => { - if (currentView === 'pageEditor') return pageEditorTotalPages; + if (currentView === "pageEditor") return pageEditorTotalPages; return activeFiles.length; }, [currentView, pageEditorTotalPages, activeFiles.length]); const selectedCount = useMemo(() => { - if (currentView === 'pageEditor') { + if (currentView === "pageEditor") { return pageEditorSelectedCount; } return selectedFileIds.length; }, [currentView, pageEditorSelectedCount, selectedFileIds.length]); const sectionsWithButtons = useMemo(() => { - return SECTION_ORDER - .map(section => { - const sectionButtons = buttons.filter(btn => (btn.section ?? 'top') === section && (btn.visible ?? true)); - return { section, buttons: sectionButtons }; - }) - .filter(entry => entry.buttons.length > 0); + return SECTION_ORDER.map((section) => { + const sectionButtons = buttons.filter((btn) => (btn.section ?? "top") === section && (btn.visible ?? true)); + return { section, buttons: sectionButtons }; + }).filter((entry) => entry.buttons.length > 0); }, [buttons]); const renderButton = useCallback( @@ -110,20 +108,19 @@ export default function RightRail() { if (!btn.icon) return null; - const ariaLabel = - btn.ariaLabel || (typeof btn.tooltip === 'string' ? (btn.tooltip as string) : undefined); - const className = ['right-rail-icon', btn.className].filter(Boolean).join(' '); + const ariaLabel = btn.ariaLabel || (typeof btn.tooltip === "string" ? (btn.tooltip as string) : undefined); + const className = ["right-rail-icon", btn.className].filter(Boolean).join(" "); const buttonNode = ( {btn.icon} @@ -131,12 +128,12 @@ export default function RightRail() { return renderWithTooltip(buttonNode, btn.tooltip, tooltipPosition, tooltipOffset); }, - [actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset] + [actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset], ); const handleExportAll = useCallback( async (forceNewFile = false) => { - if (currentView === 'viewer') { + if (currentView === "viewer") { const buffer = await viewerContext?.exportActions?.saveAsCopy?.(); if (!buffer) return; const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0]; @@ -144,7 +141,7 @@ export default function RightRail() { const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined; try { const result = await downloadFile({ - data: new Blob([buffer], { type: 'application/pdf' }), + data: new Blob([buffer], { type: "application/pdf" }), filename: fileToExport.name, localPath: forceNewFile ? undefined : stub?.localFilePath, }); @@ -155,12 +152,12 @@ export default function RightRail() { }); } } catch (error) { - console.error('[RightRail] Failed to export viewer file:', error); + console.error("[RightRail] Failed to export viewer file:", error); } return; } - if (currentView === 'pageEditor') { + if (currentView === "pageEditor") { pageEditorFunctions?.onExportAll?.(); return; } @@ -184,27 +181,19 @@ export default function RightRail() { }); } } catch (error) { - console.error('[RightRail] Failed to export file:', file.name, error); + console.error("[RightRail] Failed to export file:", file.name, error); } } } }, - [ - currentView, - selectedFiles, - activeFiles, - pageEditorFunctions, - viewerContext, - selectors, - fileActions, - ] + [currentView, selectedFiles, activeFiles, pageEditorFunctions, viewerContext, selectors, fileActions], ); const downloadTooltip = useMemo(() => { - if (currentView === 'pageEditor') { - return t('rightRail.exportAll', 'Export PDF'); + if (currentView === "pageEditor") { + return t("rightRail.exportAll", "Export PDF"); } - if (currentView === 'viewer') { + if (currentView === "viewer") { return terminology.download; } if (selectedCount > 0) { @@ -223,11 +212,7 @@ export default function RightRail() { const content = renderButton(btn); if (!content) return null; return ( -
+
{content}
); @@ -236,31 +221,24 @@ export default function RightRail() { ))} -
+
{renderWithTooltip( - - {themeMode === 'dark' ? ( - + + {themeMode === "dark" ? ( + ) : ( - + )} , - t('rightRail.toggleTheme', 'Toggle Theme'), + t("rightRail.toggleTheme", "Toggle Theme"), tooltipPosition, - tooltipOffset + tooltipOffset, )} - + {renderWithTooltip( handleExportAll()} - disabled={ - disableForFullscreen || - (currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled)) - } + disabled={disableForFullscreen || (currentView !== "viewer" && (totalItems === 0 || allButtonsDisabled))} > , downloadTooltip, tooltipPosition, - tooltipOffset + tooltipOffset, )} {icons.saveAsIconName && renderWithTooltip( @@ -286,16 +261,13 @@ export default function RightRail() { radius="md" className="right-rail-icon" onClick={() => handleExportAll(true)} - disabled={ - disableForFullscreen || - (currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled)) - } + disabled={disableForFullscreen || (currentView !== "viewer" && (totalItems === 0 || allButtonsDisabled))} > , - t('rightRail.saveAs', 'Save As'), + t("rightRail.saveAs", "Save As"), tooltipPosition, - tooltipOffset + tooltipOffset, )}
diff --git a/frontend/src/core/components/shared/ShareFileModal.tsx b/frontend/src/core/components/shared/ShareFileModal.tsx index 3cdcf99245..757aa86bc7 100644 --- a/frontend/src/core/components/shared/ShareFileModal.tsx +++ b/frontend/src/core/components/shared/ShareFileModal.tsx @@ -1,19 +1,19 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Modal, Stack, Text, Button, Group, Alert, TextInput, Paper, Select } from '@mantine/core'; -import LinkIcon from '@mui/icons-material/Link'; -import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Modal, Stack, Text, Button, Group, Alert, TextInput, Paper, Select } from "@mantine/core"; +import LinkIcon from "@mui/icons-material/Link"; +import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded"; +import { useTranslation } from "react-i18next"; -import apiClient from '@app/services/apiClient'; -import { absoluteWithBasePath } from '@app/constants/app'; -import { alert } from '@app/components/toast'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { uploadHistoryChain } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; +import apiClient from "@app/services/apiClient"; +import { absoluteWithBasePath } from "@app/constants/app"; +import { alert } from "@app/components/toast"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { uploadHistoryChain } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; interface ShareFileModalProps { opened: boolean; @@ -22,12 +22,7 @@ interface ShareFileModalProps { onUploaded?: () => Promise | void; } -const ShareFileModal: React.FC = ({ - opened, - onClose, - file, - onUploaded, -}) => { +const ShareFileModal: React.FC = ({ opened, onClose, file, onUploaded }) => { const { t } = useTranslation(); const { config } = useAppConfig(); const { actions } = useFileActions(); @@ -35,7 +30,7 @@ const ShareFileModal: React.FC = ({ const [isWorking, setIsWorking] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [shareToken, setShareToken] = useState(null); - const [shareRole, setShareRole] = useState<'editor' | 'commenter' | 'viewer'>('editor'); + const [shareRole, setShareRole] = useState<"editor" | "commenter" | "viewer">("editor"); useEffect(() => { if (!opened) { @@ -47,18 +42,18 @@ const ShareFileModal: React.FC = ({ useEffect(() => { if (opened) { - setShareRole('editor'); + setShareRole("editor"); } }, [opened]); const shareUrl = useMemo(() => { - if (!shareToken) return ''; - const frontendUrl = (config?.frontendUrl || '').trim(); + if (!shareToken) return ""; + const frontendUrl = (config?.frontendUrl || "").trim(); if (frontendUrl) { try { const parsed = new URL(frontendUrl); - if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { - const normalized = frontendUrl.endsWith('/') ? frontendUrl.slice(0, -1) : frontendUrl; + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + const normalized = frontendUrl.endsWith("/") ? frontendUrl.slice(0, -1) : frontendUrl; return `${normalized}/share/${shareToken}`; } } catch { @@ -68,18 +63,21 @@ const ShareFileModal: React.FC = ({ return absoluteWithBasePath(`/share/${shareToken}`); }, [config?.frontendUrl, shareToken]); - const createShareLink = useCallback(async (storedFileId: number) => { - const response = await apiClient.post(`/api/v1/storage/files/${storedFileId}/shares/links`, { - accessRole: shareRole, - }); - return response.data as { token?: string }; - }, [shareRole]); + const createShareLink = useCallback( + async (storedFileId: number) => { + const response = await apiClient.post(`/api/v1/storage/files/${storedFileId}/shares/links`, { + accessRole: shareRole, + }); + return response.data as { token?: string }; + }, + [shareRole], + ); const handleGenerateLink = useCallback(async () => { if (!shareLinksEnabled) { alert({ - alertType: 'warning', - title: t('storageShare.linksDisabled', 'Share links are disabled.'), + alertType: "warning", + title: t("storageShare.linksDisabled", "Share links are disabled."), expandable: false, durationMs: 2500, }); @@ -101,10 +99,7 @@ const ShareFileModal: React.FC = ({ if (!isUpToDate) { const originalFileId = (file.originalFileId || file.id) as FileId; const remoteId = file.remoteStorageId; - const { remoteId: newStoredId, updatedAt, chain } = await uploadHistoryChain( - originalFileId, - remoteId - ); + const { remoteId: newStoredId, updatedAt, chain } = await uploadHistoryChain(originalFileId, remoteId); storedId = newStoredId; for (const stub of chain) { @@ -124,14 +119,14 @@ const ShareFileModal: React.FC = ({ } if (!storedId) { - throw new Error('Missing stored file ID for sharing.'); + throw new Error("Missing stored file ID for sharing."); } const shareResponse = await createShareLink(storedId); setShareToken(shareResponse.token ?? null); alert({ - alertType: 'success', - title: t('storageShare.generated', 'Share link generated'), + alertType: "success", + title: t("storageShare.generated", "Share link generated"), expandable: false, durationMs: 3000, }); @@ -143,10 +138,8 @@ const ShareFileModal: React.FC = ({ await onUploaded(); } } catch (error: any) { - console.error('Failed to generate share link:', error); - setErrorMessage( - t('storageShare.failure', 'Unable to generate a share link. Please try again.') - ); + console.error("Failed to generate share link:", error); + setErrorMessage(t("storageShare.failure", "Unable to generate a share link. Please try again.")); } finally { setIsWorking(false); } @@ -157,16 +150,16 @@ const ShareFileModal: React.FC = ({ try { await navigator.clipboard.writeText(shareUrl); alert({ - alertType: 'success', - title: t('storageShare.copied', 'Link copied to clipboard'), + alertType: "success", + title: t("storageShare.copied", "Link copied to clipboard"), expandable: false, durationMs: 2000, }); } catch (error) { - console.error('Failed to copy share link:', error); + console.error("Failed to copy share link:", error); alert({ - alertType: 'warning', - title: t('storageShare.copyFailed', 'Copy failed'), + alertType: "warning", + title: t("storageShare.copyFailed", "Copy failed"), expandable: false, durationMs: 2500, }); @@ -178,7 +171,7 @@ const ShareFileModal: React.FC = ({ opened={opened} onClose={onClose} centered - title={t('storageShare.title', 'Share File')} + title={t("storageShare.title", "Share File")} zIndex={Z_INDEX_OVER_FILE_MANAGER_MODAL} size="lg" overlayProps={{ blur: 6 }} @@ -188,24 +181,24 @@ const ShareFileModal: React.FC = ({ {t( - 'storageShare.description', - 'Create a share link for this file. Signed-in users with the link can access it.' + "storageShare.description", + "Create a share link for this file. Signed-in users with the link can access it.", )} - {t('storageShare.fileLabel', 'File')}: {file.name} + {t("storageShare.fileLabel", "File")}: {file.name} {errorMessage && ( - + {errorMessage} )} {!shareLinksEnabled && ( - - {t('storageShare.linksDisabledBody', 'Share links are disabled by your server settings.')} + + {t("storageShare.linksDisabledBody", "Share links are disabled by your server settings.")} )} @@ -215,7 +208,7 @@ const ShareFileModal: React.FC = ({ = ({ leftSection={} onClick={handleCopyLink} > - {t('storageShare.copy', 'Copy')} + {t("storageShare.copy", "Copy")} } /> @@ -234,22 +227,22 @@ const ShareFileModal: React.FC = ({ - {t('storageShare.linkAccessTitle', 'Share link access')} + {t("storageShare.linkAccessTitle", "Share link access")} setShareRole((value as typeof shareRole) || 'editor')} + onChange={(value) => setShareRole((value as typeof shareRole) || "editor")} comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10 }} data={[ - { value: 'editor', label: t('storageShare.roleEditor', 'Editor') }, - { value: 'commenter', label: t('storageShare.roleCommenter', 'Commenter') }, - { value: 'viewer', label: t('storageShare.roleViewer', 'Viewer') }, + { value: "editor", label: t("storageShare.roleEditor", "Editor") }, + { value: "commenter", label: t("storageShare.roleCommenter", "Commenter") }, + { value: "viewer", label: t("storageShare.roleViewer", "Viewer") }, ]} /> - {shareRole === 'commenter' && ( + {shareRole === "commenter" && ( - {t('storageShare.commenterHint', 'Commenting is coming soon.')} + {t("storageShare.commenterHint", "Commenting is coming soon.")} )} @@ -454,7 +432,7 @@ const ShareManagementModal: React.FC = ({ onClick={() => createShareLink()} loading={isLoading} > - {t('storageShare.generate', 'Generate Link')} + {t("storageShare.generate", "Generate Link")} @@ -464,19 +442,19 @@ const ShareManagementModal: React.FC = ({ - {t('storageShare.sharedUsersTitle', 'Shared users')} + {t("storageShare.sharedUsersTitle", "Shared users")} { setShareUsername(event.currentTarget.value); setShowEmailWarning(false); }} onKeyDown={(event) => { - if (event.key === 'Enter') { + if (event.key === "Enter") { event.preventDefault(); void handleAddUser(); } @@ -488,31 +466,24 @@ const ShareManagementModal: React.FC = ({ onClick={() => handleAddUser()} disabled={!sharingEnabled || isLoading || !normalizedShareUsername || !!shareUsernameError} > - {t('storageShare.addUser', 'Add')} + {t("storageShare.addUser", "Add")} {showEmailWarning && ( - + {t( - 'storageShare.emailWarningBody', - 'This looks like an email address. If this person is not already a Stirling PDF user, they will not be able to access the file.' + "storageShare.emailWarningBody", + "This looks like an email address. If this person is not already a Stirling PDF user, they will not be able to access the file.", )} - - @@ -520,7 +491,7 @@ const ShareManagementModal: React.FC = ({ )} {sharedUsers.length === 0 ? ( - {t('storageShare.noSharedUsers', 'No users have access yet.')} + {t("storageShare.noSharedUsers", "No users have access yet.")} ) : ( @@ -528,24 +499,24 @@ const ShareManagementModal: React.FC = ({ {user.username} - {user.accessRole === 'commenter' && ( + {user.accessRole === "commenter" && ( - {t('storageShare.commenterHint', 'Commenting is coming soon.')} + {t("storageShare.commenterHint", "Commenting is coming soon.")} )} onChange(e.currentTarget.value)} - autoComplete={autoComplete} - className={styles.input} - disabled={disabled} - readOnly={readOnly} - aria-label={ariaLabel} - onFocus={onFocus} - style={{ - backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', - color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', - paddingRight: shouldShowClearButton ? '40px' : '12px', - paddingLeft: icon ? '40px' : '12px', - }} - {...props} - /> - {shouldShowClearButton && ( - - )} -
- ); -}); + return ( +
+ {icon && ( + + {icon} + + )} + onChange(e.currentTarget.value)} + autoComplete={autoComplete} + className={styles.input} + disabled={disabled} + readOnly={readOnly} + aria-label={ariaLabel} + onFocus={onFocus} + style={{ + backgroundColor: colorScheme === "dark" ? "#4B525A" : "#FFFFFF", + color: colorScheme === "dark" ? "#FFFFFF" : "#6B7382", + paddingRight: shouldShowClearButton ? "40px" : "12px", + paddingLeft: icon ? "40px" : "12px", + }} + {...props} + /> + {shouldShowClearButton && ( + + )} +
+ ); + }, +); -TextInput.displayName = 'TextInput'; +TextInput.displayName = "TextInput"; diff --git a/frontend/src/core/components/shared/ToolChain.tsx b/frontend/src/core/components/shared/ToolChain.tsx index c67d894273..7d2d45c6c2 100644 --- a/frontend/src/core/components/shared/ToolChain.tsx +++ b/frontend/src/core/components/shared/ToolChain.tsx @@ -3,63 +3,66 @@ * Used across FileListItem, FileDetails, and FileThumbnail for consistent display */ -import React from 'react'; -import { Text, Tooltip, Badge, Group } from '@mantine/core'; -import { ToolOperation } from '@app/types/file'; -import { useTranslation } from 'react-i18next'; -import { ToolId } from '@app/types/toolId'; +import React from "react"; +import { Text, Tooltip, Badge, Group } from "@mantine/core"; +import { ToolOperation } from "@app/types/file"; +import { useTranslation } from "react-i18next"; +import { ToolId } from "@app/types/toolId"; interface ToolChainProps { toolChain: ToolOperation[]; maxWidth?: string; - displayStyle?: 'text' | 'badges' | 'compact'; - size?: 'xs' | 'sm' | 'md'; + displayStyle?: "text" | "badges" | "compact"; + size?: "xs" | "sm" | "md"; color?: string; } const ToolChain: React.FC = ({ toolChain, - maxWidth = '100%', - displayStyle = 'text', - size = 'xs', - color = 'var(--mantine-color-blue-7)' + maxWidth = "100%", + displayStyle = "text", + size = "xs", + color = "var(--mantine-color-blue-7)", }) => { const { t } = useTranslation(); if (!toolChain || toolChain.length === 0) return null; - const toolIds = toolChain.map(tool => tool.toolId); + const toolIds = toolChain.map((tool) => tool.toolId); const getToolName = (toolId: ToolId) => { return t(`home.${toolId}.title`, toolId); }; // Create full tool chain for tooltip - const fullChainDisplay = displayStyle === 'badges' ? ( - - {toolChain.map((tool, index) => ( - - - {getToolName(tool.toolId)} - - {index < toolChain.length - 1 && ( - - )} - - ))} - - ) : ( - {toolIds.map(getToolName).join(' → ')} - ); + const fullChainDisplay = + displayStyle === "badges" ? ( + + {toolChain.map((tool, index) => ( + + + {getToolName(tool.toolId)} + + {index < toolChain.length - 1 && ( + + → + + )} + + ))} + + ) : ( + {toolIds.map(getToolName).join(" → ")} + ); // Create truncated display based on available space const getTruncatedDisplay = () => { if (toolIds.length <= 2) { // Show all tools if 2 or fewer - return { text: toolIds.map(getToolName).join(' → '), isTruncated: false }; + return { text: toolIds.map(getToolName).join(" → "), isTruncated: false }; } else { // Show first tool ... last tool for longer chains return { - text: `${getToolName(toolIds[0])} → +${toolIds.length-2} → ${getToolName(toolIds[toolIds.length - 1])}`, + text: `${getToolName(toolIds[0])} → +${toolIds.length - 2} → ${getToolName(toolIds[toolIds.length - 1])}`, isTruncated: true, }; } @@ -68,7 +71,7 @@ const ToolChain: React.FC = ({ const { text: truncatedText, isTruncated } = getTruncatedDisplay(); // Compact style for very small spaces - if (displayStyle === 'compact') { + if (displayStyle === "compact") { const compactText = toolIds.length === 1 ? getToolName(toolIds[0]) : `${toolIds.length} tools`; const isCompactTruncated = toolIds.length > 1; @@ -78,11 +81,11 @@ const ToolChain: React.FC = ({ style={{ color, fontWeight: 500, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", maxWidth: `${maxWidth}`, - cursor: isCompactTruncated ? 'help' : 'default' + cursor: isCompactTruncated ? "help" : "default", }} > {compactText} @@ -93,15 +96,17 @@ const ToolChain: React.FC = ({ {compactElement} - ) : compactElement; + ) : ( + compactElement + ); } // Badge style for file details - if (displayStyle === 'badges') { + if (displayStyle === "badges") { const isBadgesTruncated = toolChain.length > 3; const badgesElement = ( -
+
{toolChain.slice(0, 3).map((tool, index) => ( @@ -109,13 +114,17 @@ const ToolChain: React.FC = ({ {getToolName(tool.toolId)} {index < Math.min(toolChain.length - 1, 2) && ( - + + → + )} ))} {toolChain.length > 3 && ( <> - ... + + ... + {getToolName(toolChain[toolChain.length - 1].toolId)} @@ -126,10 +135,12 @@ const ToolChain: React.FC = ({ ); return isBadgesTruncated ? ( - + {badgesElement} - ) : badgesElement; + ) : ( + badgesElement + ); } // Text style (default) for file list items @@ -139,11 +150,11 @@ const ToolChain: React.FC = ({ style={{ color, fontWeight: 500, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", maxWidth: `${maxWidth}`, - cursor: isTruncated ? 'help' : 'default' + cursor: isTruncated ? "help" : "default", }} > {truncatedText} @@ -154,7 +165,9 @@ const ToolChain: React.FC = ({ {textElement} - ) : textElement; + ) : ( + textElement + ); }; export default ToolChain; diff --git a/frontend/src/core/components/shared/ToolIcon.tsx b/frontend/src/core/components/shared/ToolIcon.tsx index 75ab249ba7..d0a1c82b9b 100644 --- a/frontend/src/core/components/shared/ToolIcon.tsx +++ b/frontend/src/core/components/shared/ToolIcon.tsx @@ -15,7 +15,7 @@ export const ToolIcon: React.FC = ({ icon, opacity = 1, color = "var(--tools-text-and-icon-color)", - marginRight = "0.5rem" + marginRight = "0.5rem", }) => { return (
= ({ marginRight, transform: "scale(0.8)", transformOrigin: "center", - opacity + opacity, }} > {icon} diff --git a/frontend/src/core/components/shared/Tooltip.tsx b/frontend/src/core/components/shared/Tooltip.tsx index 2580b3530a..8757e56bfa 100644 --- a/frontend/src/core/components/shared/Tooltip.tsx +++ b/frontend/src/core/components/shared/Tooltip.tsx @@ -1,18 +1,18 @@ -import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; -import { createPortal } from 'react-dom'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { addEventListenerWithCleanup } from '@app/utils/genericUtils'; -import { useTooltipPosition } from '@app/hooks/useTooltipPosition'; -import { TooltipTip } from '@app/types/tips'; -import { TooltipContent } from '@app/components/shared/tooltip/TooltipContent'; -import { useSidebarContext } from '@app/contexts/SidebarContext'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import styles from '@app/components/shared/tooltip/Tooltip.module.css'; -import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; +import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; +import { createPortal } from "react-dom"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { addEventListenerWithCleanup } from "@app/utils/genericUtils"; +import { useTooltipPosition } from "@app/hooks/useTooltipPosition"; +import { TooltipTip } from "@app/types/tips"; +import { TooltipContent } from "@app/components/shared/tooltip/TooltipContent"; +import { useSidebarContext } from "@app/contexts/SidebarContext"; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; +import styles from "@app/components/shared/tooltip/Tooltip.module.css"; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex"; export interface TooltipProps { sidebarTooltip?: boolean; - position?: 'right' | 'left' | 'top' | 'bottom'; + position?: "right" | "left" | "top" | "bottom"; content?: React.ReactNode; tips?: TooltipTip[]; children: React.ReactElement; @@ -73,8 +73,7 @@ export const Tooltip: React.FC = ({ const tooltipIdRef = useRef(`tooltip-${Math.random().toString(36).slice(2)}`); // Runtime guard: some browsers may surface non-Node EventTargets for relatedTarget/target - const isDomNode = (value: unknown): value is Node => - typeof Node !== 'undefined' && value instanceof Node; + const isDomNode = (value: unknown): value is Node => typeof Node !== "undefined" && value instanceof Node; const clearTimers = useCallback(() => { if (openTimeoutRef.current) { @@ -92,14 +91,14 @@ export const Tooltip: React.FC = ({ const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled; const allowAutoClose = !manualCloseOnly; - const resolvedPosition: NonNullable = useMemo(() => { - const htmlDir = typeof document !== 'undefined' ? document.documentElement.dir : 'ltr'; - const isRTL = htmlDir === 'rtl'; - const base = position ?? 'right'; - if (!isRTL) return base as NonNullable; - if (base === 'left') return 'right'; - if (base === 'right') return 'left'; - return base as NonNullable; + const resolvedPosition: NonNullable = useMemo(() => { + const htmlDir = typeof document !== "undefined" ? document.documentElement.dir : "ltr"; + const isRTL = htmlDir === "rtl"; + const base = position ?? "right"; + if (!isRTL) return base as NonNullable; + if (base === "left") return "right"; + if (base === "right") return "left"; + return base as NonNullable; }, [position]); const setOpen = useCallback( @@ -109,7 +108,7 @@ export const Tooltip: React.FC = ({ else setInternalOpen(newOpen); if (!newOpen) setIsPinned(false); }, - [isControlled, onOpenChange, open] + [isControlled, onOpenChange, open], ); const { coords, positionReady } = useTooltipPosition({ @@ -146,13 +145,13 @@ export const Tooltip: React.FC = ({ setOpen(false); } }, - [isPinned, closeOnOutside, setOpen, allowAutoClose] + [isPinned, closeOnOutside, setOpen, allowAutoClose], ); useEffect(() => { // Attach global click when open (so hover tooltips can also close on outside if desired) if (open || isPinned) { - return addEventListenerWithCleanup(document, 'click', handleDocumentClick as EventListener); + return addEventListenerWithCleanup(document, "click", handleDocumentClick as EventListener); } }, [open, isPinned, handleDocumentClick]); @@ -160,11 +159,11 @@ export const Tooltip: React.FC = ({ const arrowClass = useMemo(() => { if (sidebarTooltip) return null; - const map: Record, string> = { - top: 'tooltip-arrow-bottom', - bottom: 'tooltip-arrow-top', - left: 'tooltip-arrow-left', - right: 'tooltip-arrow-right', + const map: Record, string> = { + top: "tooltip-arrow-bottom", + bottom: "tooltip-arrow-top", + left: "tooltip-arrow-left", + right: "tooltip-arrow-right", }; return map[resolvedPosition] || map.right; }, [resolvedPosition, sidebarTooltip]); @@ -173,8 +172,8 @@ export const Tooltip: React.FC = ({ (key: string) => styles[key as keyof typeof styles] || styles[key.replace(/-([a-z])/g, (_, l) => l.toUpperCase()) as keyof typeof styles] || - '', - [] + "", + [], ); // === Trigger handlers === @@ -189,7 +188,7 @@ export const Tooltip: React.FC = ({ if (!isPinned && !disabled) openWithDelay(); (children.props as any)?.onPointerEnter?.(e); }, - [isPinned, openWithDelay, children.props, disabled] + [isPinned, openWithDelay, children.props, disabled], ); const handlePointerLeave = useCallback( @@ -198,7 +197,6 @@ export const Tooltip: React.FC = ({ // Moving into the tooltip → keep open if (isDomNode(related) && tooltipRef.current && tooltipRef.current.contains(related)) { - (children.props as any)?.onPointerLeave?.(e); return; } @@ -213,7 +211,7 @@ export const Tooltip: React.FC = ({ if (allowAutoClose && !isPinned) setOpen(false); (children.props as any)?.onPointerLeave?.(e); }, - [clearTimers, isPinned, setOpen, children.props, allowAutoClose] + [clearTimers, isPinned, setOpen, children.props, allowAutoClose], ); const handleMouseDown = useCallback( @@ -221,7 +219,7 @@ export const Tooltip: React.FC = ({ clickPendingRef.current = true; (children.props as any)?.onMouseDown?.(e); }, - [children.props] + [children.props], ); const handleMouseUp = useCallback( @@ -230,7 +228,7 @@ export const Tooltip: React.FC = ({ queueMicrotask(() => (clickPendingRef.current = false)); (children.props as any)?.onMouseUp?.(e); }, - [children.props] + [children.props], ); const handleClick = useCallback( @@ -247,7 +245,7 @@ export const Tooltip: React.FC = ({ clickPendingRef.current = false; (children.props as any)?.onClick?.(e); }, - [clearTimers, pinOnClick, open, setOpen, children.props] + [clearTimers, pinOnClick, open, setOpen, children.props], ); // Keyboard / focus accessibility @@ -256,7 +254,7 @@ export const Tooltip: React.FC = ({ if (!isPinned && !disabled && openOnFocus) openWithDelay(); (children.props as any)?.onFocus?.(e); }, - [isPinned, openWithDelay, children.props, disabled, openOnFocus] + [isPinned, openWithDelay, children.props, disabled, openOnFocus], ); const handleBlur = useCallback( @@ -270,13 +268,16 @@ export const Tooltip: React.FC = ({ if (allowAutoClose && !isPinned) setOpen(false); (children.props as any)?.onBlur?.(e); }, - [isPinned, setOpen, children.props, allowAutoClose, clearTimers] + [isPinned, setOpen, children.props, allowAutoClose, clearTimers], ); - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (manualCloseOnly) return; - if (e.key === 'Escape') setOpen(false); - }, [setOpen, manualCloseOnly]); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (manualCloseOnly) return; + if (e.key === "Escape") setOpen(false); + }, + [setOpen, manualCloseOnly], + ); // Keep open while pointer is over the tooltip; close when leaving it (if not pinned) const handleTooltipPointerEnter = useCallback(() => { @@ -289,7 +290,7 @@ export const Tooltip: React.FC = ({ if (isDomNode(related) && triggerRef.current && triggerRef.current.contains(related)) return; if (allowAutoClose && !isPinned) setOpen(false); }, - [isPinned, setOpen, allowAutoClose] + [isPinned, setOpen, allowAutoClose], ); // Enhance child with handlers and ref @@ -297,10 +298,10 @@ export const Tooltip: React.FC = ({ ref: (node: HTMLElement | null) => { triggerRef.current = node || null; const originalRef = (children as any).ref; - if (typeof originalRef === 'function') originalRef(node); - else if (originalRef && typeof originalRef === 'object') (originalRef as any).current = node; + if (typeof originalRef === "function") originalRef(node); + else if (originalRef && typeof originalRef === "object") (originalRef as any).current = node; }, - 'aria-describedby': open ? tooltipIdRef.current : undefined, + "aria-describedby": open ? tooltipIdRef.current : undefined, onPointerEnter: handlePointerEnter, onPointerLeave: handlePointerLeave, onMouseDown: handleMouseDown, @@ -323,23 +324,30 @@ export const Tooltip: React.FC = ({ onPointerEnter={handleTooltipPointerEnter} onPointerLeave={handleTooltipPointerLeave} style={{ - position: 'fixed', + position: "fixed", top: coords.top, left: coords.left, - width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined), + width: maxWidth !== undefined ? maxWidth : sidebarTooltip ? ("25rem" as const) : undefined, minWidth, zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE, - visibility: positionReady ? 'visible' : 'hidden', + visibility: positionReady ? "visible" : "hidden", opacity: positionReady ? 1 : 0, - color: 'var(--text-primary)', + color: "var(--text-primary)", ...containerStyle, }} - className={`${styles['tooltip-container']} ${isPinned ? styles.pinned : ''}`} - onClick={pinOnClick ? (e) => { e.stopPropagation(); setIsPinned(true); } : undefined} + className={`${styles["tooltip-container"]} ${isPinned ? styles.pinned : ""}`} + onClick={ + pinOnClick + ? (e) => { + e.stopPropagation(); + setIsPinned(true); + } + : undefined + } > {shouldShowCloseButton && ( @@ -254,7 +246,7 @@ const UpdateModal: React.FC = ({ - {t('update.loadingDetailedInfo', 'Loading detailed information...')} + {t("update.loadingDetailedInfo", "Loading detailed information...")}
@@ -262,10 +254,10 @@ const UpdateModal: React.FC = ({ - {t('update.availableUpdates', 'Available Updates')} + {t("update.availableUpdates", "Available Updates")} - {fullUpdateInfo.new_versions.length} {fullUpdateInfo.new_versions.length === 1 ? 'version' : 'versions'} + {fullUpdateInfo.new_versions.length} {fullUpdateInfo.new_versions.length === 1 ? "version" : "versions"} @@ -275,9 +267,9 @@ const UpdateModal: React.FC = ({ = ({ align="center" p="md" style={{ - cursor: 'pointer', - background: isExpanded ? 'var(--mantine-color-gray-0)' : 'transparent', - transition: 'background 0.15s ease', + cursor: "pointer", + background: isExpanded ? "var(--mantine-color-gray-0)" : "transparent", + transition: "background 0.15s ease", }} onClick={() => toggleVersion(index)} > - {t('update.version', 'Version')} + {t("update.version", "Version")} {version.version} @@ -314,18 +306,18 @@ const UpdateModal: React.FC = ({ onClick={(e) => e.stopPropagation()} rightSection={} > - {t('update.releaseNotes', 'Release Notes')} + {t("update.releaseNotes", "Release Notes")} {isExpanded ? ( - + ) : ( - + )} - + @@ -339,21 +331,23 @@ const UpdateModal: React.FC = ({ {version.compatibility.breaking_changes && ( - + - {t('update.breakingChanges', 'Breaking Changes')} + {t("update.breakingChanges", "Breaking Changes")} {version.compatibility.breaking_description || - t('update.breakingChangesDefault', 'This version contains breaking changes.')} + t("update.breakingChangesDefault", "This version contains breaking changes.")} {version.compatibility.migration_guide_url && ( )} @@ -384,7 +378,7 @@ const UpdateModal: React.FC = ({ {downloadUrl && ( )} diff --git a/frontend/src/core/components/shared/UploadToServerModal.tsx b/frontend/src/core/components/shared/UploadToServerModal.tsx index ecf424e60b..5bab2d8efa 100644 --- a/frontend/src/core/components/shared/UploadToServerModal.tsx +++ b/frontend/src/core/components/shared/UploadToServerModal.tsx @@ -1,15 +1,15 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { Modal, Stack, Text, Button, Group, Alert } from '@mantine/core'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useState } from "react"; +import { Modal, Stack, Text, Button, Group, Alert } from "@mantine/core"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import { useTranslation } from "react-i18next"; -import { alert } from '@app/components/toast'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { uploadHistoryChain } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; +import { alert } from "@app/components/toast"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { uploadHistoryChain } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; interface UploadToServerModalProps { opened: boolean; @@ -18,12 +18,7 @@ interface UploadToServerModalProps { onUploaded?: () => Promise | void; } -const UploadToServerModal: React.FC = ({ - opened, - onClose, - file, - onUploaded, -}) => { +const UploadToServerModal: React.FC = ({ opened, onClose, file, onUploaded }) => { const { t } = useTranslation(); const { actions } = useFileActions(); const [isUploading, setIsUploading] = useState(false); @@ -43,10 +38,7 @@ const UploadToServerModal: React.FC = ({ try { const originalFileId = (file.originalFileId || file.id) as FileId; const remoteId = file.remoteStorageId; - const { remoteId: storedId, updatedAt, chain } = await uploadHistoryChain( - originalFileId, - remoteId - ); + const { remoteId: storedId, updatedAt, chain } = await uploadHistoryChain(originalFileId, remoteId); for (const stub of chain) { actions.updateStirlingFileStub(stub.id, { @@ -62,8 +54,8 @@ const UploadToServerModal: React.FC = ({ } alert({ - alertType: 'success', - title: t('storageUpload.success', 'Uploaded to server'), + alertType: "success", + title: t("storageUpload.success", "Uploaded to server"), expandable: false, durationMs: 3000, }); @@ -72,10 +64,8 @@ const UploadToServerModal: React.FC = ({ } onClose(); } catch (error) { - console.error('Failed to upload file to server:', error); - setErrorMessage( - t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.') - ); + console.error("Failed to upload file to server:", error); + setErrorMessage(t("storageUpload.failure", "Upload failed. Please check your login and storage settings.")); } finally { setIsUploading(false); } @@ -86,44 +76,34 @@ const UploadToServerModal: React.FC = ({ opened={opened} onClose={onClose} centered - title={t('storageUpload.title', 'Upload to Server')} + title={t("storageUpload.title", "Upload to Server")} zIndex={Z_INDEX_OVER_FILE_MANAGER_MODAL} > - {t( - 'storageUpload.description', - 'This uploads the current file to server storage for your own access.' - )} + {t("storageUpload.description", "This uploads the current file to server storage for your own access.")} - {t('storageUpload.fileLabel', 'File')}: {file.name} + {t("storageUpload.fileLabel", "File")}: {file.name} - {t( - 'storageUpload.hint', - 'Public links and access modes are controlled by your server settings.' - )} + {t("storageUpload.hint", "Public links and access modes are controlled by your server settings.")} {errorMessage && ( - + {errorMessage} )} - diff --git a/frontend/src/core/components/shared/UserSelector.tsx b/frontend/src/core/components/shared/UserSelector.tsx index 22c99ec61d..5e292bacda 100644 --- a/frontend/src/core/components/shared/UserSelector.tsx +++ b/frontend/src/core/components/shared/UserSelector.tsx @@ -1,25 +1,25 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { MultiSelect, Loader, Text, Button, Stack } from '@mantine/core'; -import { useNavigate } from 'react-router-dom'; -import { alert } from '@app/components/toast'; -import { UserSummary } from '@app/types/signingSession'; -import apiClient from '@app/services/apiClient'; -import { useAuth } from '@app/auth/UseSession'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { MultiSelect, Loader, Text, Button, Stack } from "@mantine/core"; +import { useNavigate } from "react-router-dom"; +import { alert } from "@app/components/toast"; +import { UserSummary } from "@app/types/signingSession"; +import apiClient from "@app/services/apiClient"; +import { useAuth } from "@app/auth/UseSession"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; interface UserSelectorProps { value: number[]; onChange: (userIds: number[]) => void; placeholder?: string; - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + size?: "xs" | "sm" | "md" | "lg" | "xl"; disabled?: boolean; } type SelectItem = { value: string; label: string }; type GroupedData = { group: string; items: SelectItem[] }; -const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = false }: UserSelectorProps) => { +const UserSelector = ({ value, onChange, placeholder, size = "sm", disabled = false }: UserSelectorProps) => { const { t } = useTranslation(); const { user } = useAuth(); const navigate = useNavigate(); @@ -30,8 +30,8 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa useEffect(() => { const fetchUsers = async () => { try { - const response = await apiClient.get('/api/v1/user/users'); - console.log('Users API response:', response.data); + const response = await apiClient.get("/api/v1/user/users"); + console.log("Users API response:", response.data); const fetchedUsers = response.data || []; // Process selectData inside useEffect - group by team @@ -41,16 +41,15 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa fetchedUsers .filter((u: UserSummary) => u && u.userId && u.username) .filter((u: UserSummary) => u.userId !== currentUserId) // Exclude current user - .filter((u: UserSummary) => u.teamName?.toLowerCase() !== 'internal') // Exclude internal users + .filter((u: UserSummary) => u.teamName?.toLowerCase() !== "internal") // Exclude internal users .forEach((user: UserSummary) => { - const teamName = user.teamName || t('certSign.collab.userSelector.noTeam', 'No Team'); + const teamName = user.teamName || t("certSign.collab.userSelector.noTeam", "No Team"); if (!usersByTeam[teamName]) { usersByTeam[teamName] = []; } - const displayName = user.displayName || user.username || 'Unknown'; - const username = user.username || 'unknown'; - const label = - displayName !== username ? `${displayName} (@${username})` : displayName; + const displayName = user.displayName || user.username || "Unknown"; + const username = user.username || "unknown"; + const label = displayName !== username ? `${displayName} (@${username})` : displayName; usersByTeam[teamName].push({ value: String(user.userId), label, @@ -63,14 +62,14 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa items: items.sort((a, b) => a.label.localeCompare(b.label)), })); - console.log('Processed selectData:', processed); + console.log("Processed selectData:", processed); setSelectData(processed); } catch (error) { - console.error('Failed to load users:', error); + console.error("Failed to load users:", error); alert({ - alertType: 'error', - title: t('common.error'), - body: t('certSign.collab.userSelector.loadError', 'Failed to load users'), + alertType: "error", + title: t("common.error"), + body: t("certSign.collab.userSelector.loadError", "Failed to load users"), }); } finally { setLoading(false); @@ -83,8 +82,8 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa // Process stringValue when value prop changes useEffect(() => { const safeValue = Array.isArray(value) ? value : []; - const result = safeValue.map((id) => (id != null ? id.toString() : '')).filter(Boolean); - console.log('stringValue for MultiSelect:', result); + const result = safeValue.map((id) => (id != null ? id.toString() : "")).filter(Boolean); + console.log("stringValue for MultiSelect:", result); setStringValue(result); }, [value]); @@ -97,10 +96,10 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa return ( - {t('certSign.collab.userSelector.noUsers', 'No other users found.')} + {t("certSign.collab.userSelector.noUsers", "No other users found.")} - ); @@ -111,12 +110,10 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa data={selectData} value={stringValue} onChange={(selectedIds) => { - const parsedIds = selectedIds - .map((id) => parseInt(id, 10)) - .filter((id) => !isNaN(id)); + const parsedIds = selectedIds.map((id) => parseInt(id, 10)).filter((id) => !isNaN(id)); onChange(parsedIds); }} - placeholder={placeholder || t('certSign.collab.userSelector.placeholder', 'Select users...')} + placeholder={placeholder || t("certSign.collab.userSelector.placeholder", "Select users...")} searchable clearable size={size} diff --git a/frontend/src/core/components/shared/ZipWarningModal.tsx b/frontend/src/core/components/shared/ZipWarningModal.tsx index 909cf1b31b..7a6e32c971 100644 --- a/frontend/src/core/components/shared/ZipWarningModal.tsx +++ b/frontend/src/core/components/shared/ZipWarningModal.tsx @@ -15,9 +15,9 @@ interface ZipWarningModalProps { const WARNING_ICON_STYLE: CSSProperties = { fontSize: 36, - display: 'block', - margin: '0 auto 8px', - color: 'var(--mantine-color-blue-6)' + display: "block", + margin: "0 auto 8px", + color: "var(--mantine-color-blue-6)", }; const ZipWarningModal = ({ opened, onConfirm, onCancel, fileCount, zipFileName }: ZipWarningModalProps) => { @@ -41,7 +41,7 @@ const ZipWarningModal = ({ opened, onConfirm, onCancel, fileCount, zipFileName } {t("zipWarning.message", { count: fileCount, - defaultValue: "This ZIP contains {{count}} files. Extract anyway?" + defaultValue: "This ZIP contains {{count}} files. Extract anyway?", })} diff --git a/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx b/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx index f16e38f4ea..d540bc989f 100644 --- a/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx +++ b/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx @@ -1,6 +1,6 @@ -import { Alert, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { Alert, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; interface LoginRequiredBannerProps { show: boolean; @@ -18,20 +18,26 @@ export default function LoginRequiredBanner({ show }: LoginRequiredBannerProps) return ( } - title={t('admin.settings.loginDisabled.title', 'Login Mode Required')} + title={t("admin.settings.loginDisabled.title", "Login Mode Required")} color="blue" variant="light" styles={{ root: { - borderLeft: '4px solid var(--mantine-color-blue-6)' - } + borderLeft: "4px solid var(--mantine-color-blue-6)", + }, }} > - {t('admin.settings.loginDisabled.message', 'Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.')} + {t( + "admin.settings.loginDisabled.message", + "Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.", + )} - {t('admin.settings.loginDisabled.readOnly', 'The settings below show example values for reference. Enable login mode to view and edit actual configuration.')} + {t( + "admin.settings.loginDisabled.readOnly", + "The settings below show example values for reference. Enable login mode to view and edit actual configuration.", + )} ); diff --git a/frontend/src/core/components/shared/config/OverviewHeader.tsx b/frontend/src/core/components/shared/config/OverviewHeader.tsx index 7be820620a..fe694d29b8 100644 --- a/frontend/src/core/components/shared/config/OverviewHeader.tsx +++ b/frontend/src/core/components/shared/config/OverviewHeader.tsx @@ -1,14 +1,16 @@ -import { Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; +import { Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; export function OverviewHeader() { const { t } = useTranslation(); return (
- {t('config.overview.title', 'Application Configuration')} + + {t("config.overview.title", "Application Configuration")} + - {t('config.overview.description', 'Current application settings and configuration details.')} + {t("config.overview.description", "Current application settings and configuration details.")}
); diff --git a/frontend/src/core/components/shared/config/PendingBadge.tsx b/frontend/src/core/components/shared/config/PendingBadge.tsx index cdb3306f80..625c2499e2 100644 --- a/frontend/src/core/components/shared/config/PendingBadge.tsx +++ b/frontend/src/core/components/shared/config/PendingBadge.tsx @@ -1,22 +1,22 @@ -import { Badge } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; +import { Badge } from "@mantine/core"; +import { useTranslation } from "react-i18next"; interface PendingBadgeProps { show: boolean; - size?: 'xs' | 'sm' | 'md' | 'lg'; + size?: "xs" | "sm" | "md" | "lg"; } /** * Badge to show when a setting has been saved but requires restart to take effect. */ -export default function PendingBadge({ show, size = 'xs' }: PendingBadgeProps) { +export default function PendingBadge({ show, size = "xs" }: PendingBadgeProps) { const { t } = useTranslation(); if (!show) return null; return ( - {t('admin.settings.restartRequired', 'Restart Required')} + {t("admin.settings.restartRequired", "Restart Required")} ); } diff --git a/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx b/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx index b97b17a0c6..9f8c75cee3 100644 --- a/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx +++ b/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx @@ -1,8 +1,8 @@ -import { Modal, Text, Group, Button, Stack } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import ScheduleIcon from '@mui/icons-material/Schedule'; -import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; +import { Modal, Text, Group, Button, Stack } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import ScheduleIcon from "@mui/icons-material/Schedule"; +import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; interface RestartConfirmationModalProps { opened: boolean; @@ -10,11 +10,7 @@ interface RestartConfirmationModalProps { onRestart: () => void; } -export default function RestartConfirmationModal({ - opened, - onClose, - onRestart, -}: RestartConfirmationModalProps) { +export default function RestartConfirmationModal({ opened, onClose, onRestart }: RestartConfirmationModalProps) { const { t } = useTranslation(); return ( @@ -23,7 +19,7 @@ export default function RestartConfirmationModal({ onClose={onClose} title={ - {t('admin.settings.restart.title', 'Restart Required')} + {t("admin.settings.restart.title", "Restart Required")} } centered @@ -34,32 +30,21 @@ export default function RestartConfirmationModal({ {t( - 'admin.settings.restart.message', - 'Settings have been saved successfully. A server restart is required for the changes to take effect.' + "admin.settings.restart.message", + "Settings have been saved successfully. A server restart is required for the changes to take effect.", )} - {t( - 'admin.settings.restart.question', - 'Would you like to restart the server now or later?' - )} + {t("admin.settings.restart.question", "Would you like to restart the server now or later?")} - - diff --git a/frontend/src/core/components/shared/config/SettingsSearchBar.tsx b/frontend/src/core/components/shared/config/SettingsSearchBar.tsx index cbbbd97076..b08a8b6184 100644 --- a/frontend/src/core/components/shared/config/SettingsSearchBar.tsx +++ b/frontend/src/core/components/shared/config/SettingsSearchBar.tsx @@ -1,10 +1,10 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import { Select, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { NavKey, VALID_NAV_KEYS } from '@app/components/shared/config/types'; -import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; -import type { ConfigNavSection, ConfigNavItem } from '@app/components/shared/config/configNavSections'; +import React, { useMemo, useState, useCallback } from "react"; +import { Select, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { NavKey, VALID_NAV_KEYS } from "@app/components/shared/config/types"; +import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; +import type { ConfigNavSection, ConfigNavItem } from "@app/components/shared/config/configNavSections"; interface SettingsSearchBarProps { configNavSections: ConfigNavSection[]; @@ -22,35 +22,35 @@ interface SettingsSearchOption { } const SETTINGS_SEARCH_TRANSLATION_PREFIXES: Partial> = { - general: ['settings.general'], - hotkeys: ['settings.hotkeys'], - account: ['account'], - people: ['settings.workspace'], - teams: ['settings.workspace', 'settings.team'], - 'api-keys': ['settings.developer'], - connectionMode: ['settings.connection'], - planBilling: ['settings.planBilling'], - adminGeneral: ['admin.settings.general'], - adminFeatures: ['admin.settings.features'], - adminEndpoints: ['admin.settings.endpoints'], - adminDatabase: ['admin.settings.database'], - adminAdvanced: ['admin.settings.advanced'], - adminSecurity: ['admin.settings.security'], + general: ["settings.general"], + hotkeys: ["settings.hotkeys"], + account: ["account"], + people: ["settings.workspace"], + teams: ["settings.workspace", "settings.team"], + "api-keys": ["settings.developer"], + connectionMode: ["settings.connection"], + planBilling: ["settings.planBilling"], + adminGeneral: ["admin.settings.general"], + adminFeatures: ["admin.settings.features"], + adminEndpoints: ["admin.settings.endpoints"], + adminDatabase: ["admin.settings.database"], + adminAdvanced: ["admin.settings.advanced"], + adminSecurity: ["admin.settings.security"], adminConnections: [ - 'admin.settings.connections', - 'admin.settings.mail', - 'admin.settings.security', - 'admin.settings.telegram', - 'admin.settings.premium', - 'admin.settings.general', - 'settings.securityAuth', - 'settings.connection', + "admin.settings.connections", + "admin.settings.mail", + "admin.settings.security", + "admin.settings.telegram", + "admin.settings.premium", + "admin.settings.general", + "settings.securityAuth", + "settings.connection", ], - adminPlan: ['settings.planBilling', 'admin.settings.premium', 'settings.licensingAnalytics'], - adminAudit: ['settings.licensingAnalytics'], - adminUsage: ['settings.licensingAnalytics'], - adminLegal: ['admin.settings.legal'], - adminPrivacy: ['admin.settings.privacy'], + adminPlan: ["settings.planBilling", "admin.settings.premium", "settings.licensingAnalytics"], + adminAudit: ["settings.licensingAnalytics"], + adminUsage: ["settings.licensingAnalytics"], + adminLegal: ["admin.settings.legal"], + adminPrivacy: ["admin.settings.privacy"], }; const getTranslationPrefixesForNavKey = (key: string): string[] => { @@ -58,8 +58,8 @@ const getTranslationPrefixesForNavKey = (key: string): string[] => { const inferredPrefixes: string[] = []; - if (key.startsWith('admin')) { - const adminSuffix = key.replace(/^admin/, ''); + if (key.startsWith("admin")) { + const adminSuffix = key.replace(/^admin/, ""); const normalizedAdminSuffix = adminSuffix.charAt(0).toLowerCase() + adminSuffix.slice(1); inferredPrefixes.push(`admin.settings.${normalizedAdminSuffix}`); } else { @@ -70,7 +70,7 @@ const getTranslationPrefixesForNavKey = (key: string): string[] => { }; const flattenTranslationStrings = (value: unknown): string[] => { - if (typeof value === 'string') { + if (typeof value === "string") { const trimmed = value.trim(); return trimmed ? [trimmed] : []; } @@ -79,7 +79,7 @@ const flattenTranslationStrings = (value: unknown): string[] => { return value.flatMap(flattenTranslationStrings); } - if (value && typeof value === 'object') { + if (value && typeof value === "object") { return Object.values(value as Record).flatMap(flattenTranslationStrings); } @@ -102,19 +102,15 @@ const buildMatchSnippet = (text: string, query: string): string => { const snippet = text.slice(start, end); if (snippet.length <= maxLength) { - return `${start > 0 ? '…' : ''}${snippet}${end < text.length ? '…' : ''}`; + return `${start > 0 ? "…" : ""}${snippet}${end < text.length ? "…" : ""}`; } - return `${start > 0 ? '…' : ''}${snippet.slice(0, maxLength)}${end < text.length ? '…' : ''}`; + return `${start > 0 ? "…" : ""}${snippet.slice(0, maxLength)}${end < text.length ? "…" : ""}`; }; -export const SettingsSearchBar: React.FC = ({ - configNavSections, - onNavigate, - isMobile, -}) => { +export const SettingsSearchBar: React.FC = ({ configNavSections, onNavigate, isMobile }) => { const { t } = useTranslation(); - const [searchValue, setSearchValue] = useState(''); + const [searchValue, setSearchValue] = useState(""); // Build a global index from every accessible settings tab in the modal navigation. // This does not render section components, so API calls still happen only when a tab is opened. @@ -125,16 +121,11 @@ export const SettingsSearchBar: React.FC = ({ .map((item: ConfigNavItem) => { const translationPrefixes = getTranslationPrefixesForNavKey(item.key); const translationContent = translationPrefixes.flatMap((prefix) => - flattenTranslationStrings(t(prefix, { returnObjects: true, defaultValue: {} } as any)) + flattenTranslationStrings(t(prefix, { returnObjects: true, defaultValue: {} } as any)), ); const searchableContent = Array.from( - new Set([ - item.label, - section.title, - `/settings/${item.key}`, - ...translationContent, - ]) + new Set([item.label, section.title, `/settings/${item.key}`, ...translationContent]), ); return { @@ -144,7 +135,7 @@ export const SettingsSearchBar: React.FC = ({ destinationPath: `/settings/${item.key}`, searchableContent, }; - }) + }), ); }, [configNavSections, t]); @@ -157,9 +148,7 @@ export const SettingsSearchBar: React.FC = ({ const normalizedQuery = query.toLocaleLowerCase(); return searchableSections.reduce((accumulator, option) => { - const matchedEntry = option.searchableContent.find((entry) => - entry.toLocaleLowerCase().includes(normalizedQuery) - ); + const matchedEntry = option.searchableContent.find((entry) => entry.toLocaleLowerCase().includes(normalizedQuery)); if (!matchedEntry) { return accumulator; @@ -174,12 +163,15 @@ export const SettingsSearchBar: React.FC = ({ }, []); }, [searchValue, searchableSections]); - const handleSearchNavigation = useCallback(async (value: string | null) => { - if (!value) return; - if (!VALID_NAV_KEYS.includes(value as NavKey)) return; - await onNavigate(value as NavKey); - setSearchValue(''); - }, [onNavigate]); + const handleSearchNavigation = useCallback( + async (value: string | null) => { + if (!value) return; + if (!VALID_NAV_KEYS.includes(value as NavKey)) return; + await onNavigate(value as NavKey); + setSearchValue(""); + }, + [onNavigate], + ); return (