diff --git a/src/app/globals.css b/src/app/globals.css
index 881ff04..24c7cb1 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -109,6 +109,81 @@ html.mint {
);
}
+html.lavender {
+ --background: #faf8ff;
+ --foreground: #3b2e5a;
+ --base: #f3effb;
+ --offbase: #e4daf0;
+ --accent: #7c3aed;
+ --secondary-accent: #a78bfa;
+ --muted: #8e7bab;
+ --prism-gradient: linear-gradient(90deg,
+ #c4b5fd,
+ #a78bfa,
+ #7c3aed
+ );
+}
+
+html.rose {
+ --background: #fff8f8;
+ --foreground: #4a2c2c;
+ --base: #fef1f1;
+ --offbase: #f5dada;
+ --accent: #e11d48;
+ --secondary-accent: #f472b6;
+ --muted: #b08a8a;
+ --prism-gradient: linear-gradient(90deg,
+ #fda4af,
+ #fb7185,
+ #e11d48
+ );
+}
+
+html.sand {
+ --background: #fdfbf7;
+ --foreground: #44392a;
+ --base: #f7f2e8;
+ --offbase: #e8dfc9;
+ --accent: #b45309;
+ --secondary-accent: #d97706;
+ --muted: #9a8b74;
+ --prism-gradient: linear-gradient(90deg,
+ #fcd34d,
+ #f59e0b,
+ #b45309
+ );
+}
+
+html.sky {
+ --background: #f6faff;
+ --foreground: #1e3a5f;
+ --base: #edf4fc;
+ --offbase: #d5e3f5;
+ --accent: #2563eb;
+ --secondary-accent: #3b82f6;
+ --muted: #6b8db5;
+ --prism-gradient: linear-gradient(90deg,
+ #93c5fd,
+ #60a5fa,
+ #2563eb
+ );
+}
+
+html.slate {
+ --background: #e8ecf0;
+ --foreground: #2c3440;
+ --base: #dde2e8;
+ --offbase: #c8ced6;
+ --accent: #5b7a9d;
+ --secondary-accent: #7393b0;
+ --muted: #7a8694;
+ --prism-gradient: linear-gradient(90deg,
+ #a3bdd4,
+ #7393b0,
+ #5b7a9d
+ );
+}
+
body {
color: var(--foreground);
background: var(--background);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e55cca3..a91820d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -14,7 +14,8 @@ const figtree = Figtree({
const themeInitScript = `
(() => {
- const themes = ['system', 'light', 'dark', 'ocean', 'forest', 'sunset', 'sea', 'mint'];
+ const themes = ['system', 'light', 'dark', 'ocean', 'forest', 'sunset', 'sea', 'mint', 'lavender', 'rose', 'sand', 'sky', 'slate'];
+ const lightThemes = new Set(['light', 'lavender', 'rose', 'sand', 'sky', 'slate']);
const root = document.documentElement;
const stored = localStorage.getItem('theme');
const selected = stored && themes.includes(stored) ? stored : 'system';
@@ -23,7 +24,7 @@ const themeInitScript = `
: selected;
root.classList.remove(...themes);
root.classList.add(effective);
- root.style.colorScheme = effective === 'dark' ? 'dark' : 'light';
+ root.style.colorScheme = lightThemes.has(effective) ? 'light' : 'dark';
})();
`;
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index f6f61ca..29dd750 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -39,7 +39,7 @@ export function Providers({ children, authEnabled, authBaseUrl, allowAnonymousAu
<>
{children}
-
+ {authEnabled && }
>
@@ -64,7 +64,7 @@ export function Providers({ children, authEnabled, authBaseUrl, allowAnonymousAu
<>
{children}
-
+ {authEnabled && }
>
diff --git a/src/components/PrivacyModal.tsx b/src/components/PrivacyModal.tsx
index f1b2c7a..1ea6746 100644
--- a/src/components/PrivacyModal.tsx
+++ b/src/components/PrivacyModal.tsx
@@ -13,7 +13,6 @@ import { updateAppConfig, getAppConfig } from '@/lib/client/dexie';
interface PrivacyModalProps {
onAccept?: () => void;
- authEnabled?: boolean;
}
function PrivacyModalBody({ origin }: { origin: string }) {
@@ -32,7 +31,8 @@ function PrivacyModalBody({ origin }: { origin: string }) {
- For full details on data collection, processing, and your rights, please review our complete Privacy Policy.
+ For full details on data collection, processing, and your rights, please review our complete{' '}
+ Privacy Policy .
);
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index e8248ba..4b29354 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -13,16 +13,11 @@ import {
ListboxOption,
Button,
Input,
- TabGroup,
- TabList,
- Tab,
- TabPanels,
- TabPanel,
} from '@headlessui/react';
import Link from 'next/link';
import { useTheme } from '@/contexts/ThemeContext';
import { useConfig } from '@/contexts/ConfigContext';
-import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
+import { ChevronUpDownIcon, CheckIcon, SettingsIcon, KeyIcon, PaletteIcon, DocumentIcon, UserIcon, DownloadIcon } from '@/components/icons/Icons';
import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/client/dexie';
import { useDocuments } from '@/contexts/DocumentContext';
import { ConfirmDialog } from '@/components/ConfirmDialog';
@@ -43,14 +38,47 @@ import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from
const enableDestructiveDelete = process.env.NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS !== 'false';
const showAllDeepInfra = process.env.NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS !== 'false';
+// Hard-coded theme color palettes for the visual theme selector
+type ThemeColorSet = { background: string; base: string; offbase: string; accent: string; secondaryAccent: string; foreground: string; muted: string };
-const themes = THEMES.map(id => ({
+const THEME_COLORS: Record = {
+ light: { background: '#ffffff', base: '#f7fafc', offbase: '#e2e8f0', accent: '#ef4444', secondaryAccent: '#ed6868', foreground: '#2d3748', muted: '#718096' },
+ dark: { background: '#111111', base: '#171717', offbase: '#343434', accent: '#f87171', secondaryAccent: '#eb6262', foreground: '#ededed', muted: '#a3a3a3' },
+ ocean: { background: '#020617', base: '#0f172a', offbase: '#1e293b', accent: '#38bdf8', secondaryAccent: '#22d3ee', foreground: '#e2e8f0', muted: '#94a3b8' },
+ forest: { background: '#0a0f0c', base: '#111a15', offbase: '#1a2820', accent: '#4ade80', secondaryAccent: '#22c55e', foreground: '#d4e8d0', muted: '#7c8f85' },
+ sunset: { background: '#1a0f0f', base: '#2c1810', offbase: '#3d1f14', accent: '#ff6b6b', secondaryAccent: '#f59e0b', foreground: '#ffe4d6', muted: '#bc8f8f' },
+ sea: { background: '#0c1922', base: '#102c3d', offbase: '#1a3c52', accent: '#06b6d4', secondaryAccent: '#0ea5e9', foreground: '#e0f2fe', muted: '#7ca7c4' },
+ mint: { background: '#0f1916', base: '#132d27', offbase: '#1c3d35', accent: '#2dd4bf', secondaryAccent: '#10b981', foreground: '#dcfce7', muted: '#75a99c' },
+ lavender: { background: '#faf8ff', base: '#f3effb', offbase: '#e4daf0', accent: '#7c3aed', secondaryAccent: '#a78bfa', foreground: '#3b2e5a', muted: '#8e7bab' },
+ rose: { background: '#fff8f8', base: '#fef1f1', offbase: '#f5dada', accent: '#e11d48', secondaryAccent: '#f472b6', foreground: '#4a2c2c', muted: '#b08a8a' },
+ sand: { background: '#fdfbf7', base: '#f7f2e8', offbase: '#e8dfc9', accent: '#b45309', secondaryAccent: '#d97706', foreground: '#44392a', muted: '#9a8b74' },
+ sky: { background: '#f6faff', base: '#edf4fc', offbase: '#d5e3f5', accent: '#2563eb', secondaryAccent: '#3b82f6', foreground: '#1e3a5f', muted: '#6b8db5' },
+ slate: { background: '#e8ecf0', base: '#dde2e8', offbase: '#c8ced6', accent: '#5b7a9d', secondaryAccent: '#7393b0', foreground: '#2c3440', muted: '#7a8694' },
+};
+
+const LIGHT_THEME_IDS = new Set(['light', 'lavender', 'rose', 'sand', 'sky', 'slate']);
+
+const allThemes = THEMES.map(id => ({
id,
- name: id.charAt(0).toUpperCase() + id.slice(1)
+ name: id.charAt(0).toUpperCase() + id.slice(1),
}));
+const systemTheme = allThemes.find(t => t.id === 'system')!;
+const lightThemes = allThemes.filter(t => LIGHT_THEME_IDS.has(t.id));
+const darkThemes = allThemes.filter(t => t.id !== 'system' && !LIGHT_THEME_IDS.has(t.id));
+
+type SectionId = 'api' | 'theme' | 'docs' | 'account';
+
+const SIDEBAR_SECTIONS: { id: SectionId; label: string; icon: React.ComponentType>; authOnly?: boolean }[] = [
+ { id: 'api', label: 'TTS Provider', icon: KeyIcon },
+ { id: 'theme', label: 'Appearance', icon: PaletteIcon },
+ { id: 'docs', label: 'Documents', icon: DocumentIcon },
+ { id: 'account', label: 'Account', icon: UserIcon, authOnly: true },
+];
+
export function SettingsModal({ className = '' }: { className?: string }) {
const [isOpen, setIsOpen] = useState(false);
+ const [activeSection, setActiveSection] = useState('api');
const { theme, setTheme } = useTheme();
const { apiKey, baseUrl, ttsProvider, ttsModel, ttsInstructions, updateConfig, updateConfigKey } = useConfig();
@@ -78,12 +106,10 @@ export function SettingsModal({ className = '' }: { className?: string }) {
const [showProgress, setShowProgress] = useState(false);
const [statusMessage, setStatusMessage] = useState('');
const [abortController, setAbortController] = useState(null);
- const selectedTheme = themes.find(t => t.id === theme) || themes[0];
const [showDeleteDocsConfirm, setShowDeleteDocsConfirm] = useState(false);
- const [deleteDocsMode, setDeleteDocsMode] = useState<'user' | 'unclaimed'>('user');
const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false);
const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation();
- const { authEnabled, baseUrl: authBaseUrl, allowAnonymousAuthSessions } = useAuthConfig();
+ const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig();
const { data: session } = useAuthSession();
const router = useRouter();
const isBusy = isImportingLibrary;
@@ -110,13 +136,11 @@ export function SettingsModal({ className = '' }: { className?: string }) {
{ id: 'custom', name: 'Other' }
];
case 'deepinfra':
- // In production without an API key, limit to free tier model
if (!showAllDeepInfra && !localApiKey) {
return [
{ id: 'hexgrad/Kokoro-82M', name: 'hexgrad/Kokoro-82M' }
];
}
- // In dev or with an API key, allow all models
return [
{ id: 'hexgrad/Kokoro-82M', name: 'hexgrad/Kokoro-82M' },
{ id: 'canopylabs/orpheus-3b-0.1-ft', name: 'canopylabs/orpheus-3b-0.1-ft' },
@@ -149,7 +173,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
[selectedModelId, supportsCustom, customModelInput]
);
- // Open settings on first visit (stored in Dexie app config)
const checkFirstVist = useCallback(async () => {
const appConfig = await getAppConfig();
if (!appConfig?.privacyAccepted) {
@@ -185,7 +208,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
};
}, [checkFirstVist]);
- // Detect if current model is custom (not in presets) and mirror it in the input field
useEffect(() => {
if (!ttsModels.some(m => m.id === modelValue) && modelValue !== '') {
setCustomModelInput(modelValue);
@@ -232,18 +254,11 @@ export function SettingsModal({ className = '' }: { className?: string }) {
const handleModalConfirm = async (selectedFiles: BaseDocument[]) => {
const controller = new AbortController();
setAbortController(controller);
-
- // Close modal? Maybe keep open until started?
- // Let's close it here, process starts.
- // Actually we keep it open if we want to show loading state INSIDE modal?
- // But existing UI uses a separate ProgressPopup.
- // So close modal, show popup.
setIsSelectionModalOpen(false);
try {
setShowProgress(true);
setProgress(0);
-
setIsImportingLibrary(true);
for (let i = 0; i < selectedFiles.length; i++) {
@@ -301,13 +316,8 @@ export function SettingsModal({ className = '' }: { className?: string }) {
const handleDeleteDocs = async () => {
try {
- if (deleteDocsMode === 'user') {
- await deleteDocuments();
- await refreshDocuments().catch(() => { });
- } else if (deleteDocsMode === 'unclaimed') {
- await deleteDocuments({ scope: 'unclaimed' });
- await refreshDocuments().catch(() => { });
- }
+ await deleteDocuments();
+ await refreshDocuments().catch(() => { });
} catch (error) {
console.error('Delete failed:', error);
} finally {
@@ -315,8 +325,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
}
};
-
-
const handleSignOut = async () => {
const client = getAuthClient(authBaseUrl);
await client.signOut();
@@ -328,7 +336,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
const res = await fetch('/api/account/delete', { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete account');
- // Sign out locally
const client = getAuthClient(authBaseUrl);
await client.signOut();
window.location.href = '/signup';
@@ -360,41 +367,28 @@ export function SettingsModal({ className = '' }: { className?: string }) {
}
}, [apiKey, baseUrl, ttsProvider, ttsModel, ttsInstructions, ttsModels]);
- const tabs = [
- { name: 'API', icon: '🔑' },
- { name: 'Theme', icon: '✨' },
- { name: 'Docs', icon: '📄' },
- ...(authEnabled ? [{ name: 'User', icon: '👤' }] : [])
- ];
-
- const isAnonymous = Boolean(session?.user?.isAnonymous);
-
- const userDeleteLabel = authEnabled ? (isAnonymous ? 'Delete anonymous docs' : 'Delete all user docs') : 'Delete server docs';
- const userDeleteTitle = authEnabled ? (isAnonymous ? 'Delete Anonymous Docs' : 'Delete All User Docs') : 'Delete Server Docs';
- const userDeleteMessage = authEnabled
- ? (isAnonymous
- ? 'Are you sure you want to delete all anonymous-session documents? This action cannot be undone.'
- : 'Are you sure you want to delete all of your documents from the server? This action cannot be undone.')
- : 'Are you sure you want to delete all documents from the server? This action cannot be undone.';
-
- const [unclaimedCounts, setUnclaimedCounts] = useState<{ documents: number; audiobooks: number } | null>(null);
- const canDeleteUnclaimed =
- authEnabled && session?.user && !isAnonymous && (unclaimedCounts?.documents ?? 0) > 0;
+ const [systemIsDark, setSystemIsDark] = useState(
+ typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
+ );
useEffect(() => {
- if (!authEnabled) return;
- if (!session?.user) return;
- if (session.user.isAnonymous) return;
- // fetch claimable counts (unclaimed docs/audiobooks)
- fetch('/api/user/claim')
- .then((r) => (r.ok ? r.json() : null))
- .then((data) => {
- if (data && typeof data.documents === 'number' && typeof data.audiobooks === 'number') {
- setUnclaimedCounts({ documents: data.documents, audiobooks: data.audiobooks });
- }
- })
- .catch(() => { });
- }, [authEnabled, session?.user]);
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
+ const handler = (e: MediaQueryListEvent) => setSystemIsDark(e.matches);
+ mq.addEventListener('change', handler);
+ return () => mq.removeEventListener('change', handler);
+ }, []);
+
+ const getThemeColors = useCallback((id: string): ThemeColorSet => {
+ if (id === 'system') return THEME_COLORS[systemIsDark ? 'dark' : 'light'];
+ return THEME_COLORS[id] || THEME_COLORS.light;
+ }, [systemIsDark]);
+
+ const visibleSections = SIDEBAR_SECTIONS.filter(s => !s.authOnly || authEnabled);
+
+ const btnBase = "inline-flex items-center justify-center rounded-lg text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 transform transition-transform duration-200 ease-in-out";
+ const btnPrimary = `${btnBase} bg-accent text-background hover:bg-secondary-accent hover:scale-[1.04]`;
+ const btnSecondary = `${btnBase} bg-background text-foreground hover:bg-offbase hover:text-accent hover:scale-[1.04]`;
+ const btnOutline = `${btnBase} bg-background border border-offbase text-foreground hover:bg-offbase hover:text-accent hover:scale-[1.02]`;
return (
<>
@@ -432,162 +426,94 @@ export function SettingsModal({ className = '' }: { className?: string }) {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
-
-
-
+
+ {/* Header */}
+
+
Settings
-
- showPrivacyModal({ authEnabled })}
- className="text-sm font-medium text-muted hover:text-accent"
- >
- Privacy
-
+ {authEnabled && (
+ showPrivacyModal({ authEnabled })}
+ className="text-sm font-medium text-muted hover:text-accent transform transition-transform duration-200 ease-in-out hover:scale-[1.04]"
+ >
+ Privacy
+
+ )}
-
-
- {tabs.map((tab) => (
-
- `w-full rounded-lg py-1 text-sm font-medium
- ring-accent/60 ring-offset-2 ring-offset-base
- ${selected
- ? 'bg-accent text-background shadow'
- : 'text-foreground hover:text-accent'
- }`
- }
+ {/* Mobile: 2x2 grid nav */}
+
+ {visibleSections.map((section) => {
+ const Icon = section.icon;
+ return (
+
setActiveSection(section.id)}
+ className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-medium transition-colors
+ ${activeSection === section.id
+ ? 'bg-accent text-background'
+ : 'text-foreground hover:bg-offbase hover:text-accent'
+ }`}
>
-
- {tab.icon}
- {tab.name}
-
-
- ))}
-
-
-
-
- TTS Provider
- p.id === localTTSProvider) || ttsProviders[0]}
- onChange={(provider) => {
- setLocalTTSProvider(provider.id);
- // Set default model and base_url for each provider
- if (provider.id === 'openai') {
- setModelValue('tts-1');
- setLocalBaseUrl('https://api.openai.com/v1');
- } else if (provider.id === 'custom-openai') {
- setModelValue('kokoro');
- // Clear baseUrl for custom provider
- setLocalBaseUrl('');
- } else if (provider.id === 'deepinfra') {
- setModelValue('hexgrad/Kokoro-82M');
- setLocalBaseUrl('https://api.deepinfra.com/v1/openai');
- }
- setCustomModelInput('');
- }}
- >
-
-
- {ttsProviders.find(p => p.id === localTTSProvider)?.name || 'Select Provider'}
-
-
-
-
-
-
+ {section.label}
+
+ );
+ })}
+
+
+
+ {/* Desktop: vertical sidebar */}
+
+
+ {visibleSections.map((section) => {
+ const Icon = section.icon;
+ return (
+ setActiveSection(section.id)}
+ className={`w-full flex items-center gap-2.5 text-left px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap
+ ${activeSection === section.id
+ ? 'bg-accent text-background'
+ : 'text-foreground hover:bg-offbase hover:text-accent'
+ }`}
>
-
- {ttsProviders.map((provider) => (
-
- `relative cursor-pointer select-none py-1.5 pl-10 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground'
- }`
- }
- value={provider}
- >
- {({ selected }) => (
- <>
-
- {provider.name}
-
- {selected ? (
-
-
-
- ) : null}
- >
- )}
-
- ))}
-
-
-
-
+
+ {section.label}
+
+ );
+ })}
+
+
- {(localTTSProvider === 'custom-openai' || !localBaseUrl || localBaseUrl === '') && (
-
-
- API Base URL
- {localBaseUrl && (Overriding env) }
-
-
- handleInputChange('baseUrl', e.target.value)}
- placeholder="Using environment variable"
- className="w-full rounded-lg bg-background py-1.5 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
- />
-
-
- )}
-
-
-
- API Key
- {localApiKey && (Overriding env) }
-
-
- handleInputChange('apiKey', e.target.value)}
- placeholder={!showAllDeepInfra && localTTSProvider === 'deepinfra' ? "Deepinfra free or use your API key" : "Using environment variable"}
- className="w-full rounded-lg bg-background py-1.5 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
- />
-
-
-
-
-
TTS Model
-
+ {/* Content */}
+
+ {/* API Section */}
+ {activeSection === 'api' && (
+
+
+ TTS Provider
m.id === selectedModelId) || ttsModels[0]}
- onChange={(model) => {
- if (model.id === 'custom') {
- // Switch to custom: keep the current custom input (or empty)
- setModelValue(customModelInput);
- } else {
- setModelValue(model.id);
- setCustomModelInput('');
+ value={ttsProviders.find(p => p.id === localTTSProvider) || ttsProviders[0]}
+ onChange={(provider) => {
+ setLocalTTSProvider(provider.id);
+ if (provider.id === 'openai') {
+ setModelValue('tts-1');
+ setLocalBaseUrl('https://api.openai.com/v1');
+ } else if (provider.id === 'custom-openai') {
+ setModelValue('kokoro');
+ setLocalBaseUrl('');
+ } else if (provider.id === 'deepinfra') {
+ setModelValue('hexgrad/Kokoro-82M');
+ setLocalBaseUrl('https://api.deepinfra.com/v1/openai');
}
+ setCustomModelInput('');
}}
>
-
+
- {ttsModels.find(m => m.id === selectedModelId)?.name || 'Select Model'}
+ {ttsProviders.find(p => p.id === localTTSProvider)?.name || 'Select Provider'}
@@ -599,26 +525,25 @@ export function SettingsModal({ className = '' }: { className?: string }) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
-
- {ttsModels.map((model) => (
+
+ {ttsProviders.map((provider) => (
- `relative cursor-pointer select-none py-1.5 pl-10 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground'
- }`
+ `relative cursor-pointer select-none py-2 pl-10 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground'}`
}
- value={model}
+ value={provider}
>
{({ selected }) => (
<>
- {model.name}
+ {provider.name}
- {selected ? (
+ {selected && (
- ) : null}
+ )}
>
)}
@@ -626,327 +551,443 @@ export function SettingsModal({ className = '' }: { className?: string }) {
+
- {supportsCustom && selectedModelId === 'custom' && (
+ {(localTTSProvider === 'custom-openai' || !localBaseUrl || localBaseUrl === '') && (
+
+
+ API Base URL
+ {localBaseUrl && (Overriding env) }
+
{
- setCustomModelInput(e.target.value);
- setModelValue(e.target.value);
- }}
- placeholder="Enter custom model name"
- className="w-full rounded-lg bg-background py-1.5 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
+ value={localBaseUrl}
+ onChange={(e) => handleInputChange('baseUrl', e.target.value)}
+ placeholder="Using environment variable"
+ className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
- )}
-
-
+
+ )}
- {modelValue === 'gpt-4o-mini-tts' && (
-
-
+
setShowDeleteDocsConfirm(false)}
onConfirm={handleDeleteDocs}
- title={
- deleteDocsMode === 'unclaimed'
- ? 'Delete Unclaimed Docs'
- : userDeleteTitle
- }
- message={
- deleteDocsMode === 'unclaimed'
- ? 'Are you sure you want to delete all unclaimed documents? This action cannot be undone.'
- : userDeleteMessage
- }
+ title="Delete All Data"
+ message="Are you sure you want to delete all documents from the server? This action cannot be undone."
confirmText="Delete"
isDangerous={true}
/>
@@ -985,7 +1026,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
onConfirm={handleModalConfirm}
title={selectionModalProps.title}
confirmLabel={selectionModalProps.confirmLabel}
- isProcessing={false} // Processing happens in ProgressPopup after closing
+ isProcessing={false}
defaultSelected={selectionModalProps.defaultSelected}
initialFiles={selectionModalProps.initialFiles}
fetcher={selectionModalProps.fetcher}
diff --git a/src/components/icons/Icons.tsx b/src/components/icons/Icons.tsx
index 9e0c5b1..97abb7a 100644
--- a/src/components/icons/Icons.tsx
+++ b/src/components/icons/Icons.tsx
@@ -510,6 +510,78 @@ export function GridIcon(props: React.SVGProps) {
);
}
+export function KeyIcon(props: React.SVGProps) {
+ return (
+
+
+
+ );
+}
+
+export function PaletteIcon(props: React.SVGProps) {
+ return (
+
+
+
+ );
+}
+
+export function UserIcon(props: React.SVGProps) {
+ return (
+
+
+
+ );
+}
+
+export function DocumentIcon(props: React.SVGProps) {
+ return (
+
+
+
+ );
+}
+
export function FileSettingsIcon(props: React.SVGProps) {
return (
{
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
+const LIGHT_THEMES: ReadonlySet = new Set(['light', 'lavender', 'rose', 'sand', 'sky', 'slate']);
+
const getEffectiveTheme = (theme: Theme): Theme => {
if (theme === 'system') {
return getSystemTheme();
@@ -24,6 +26,10 @@ const getEffectiveTheme = (theme: Theme): Theme => {
return theme;
};
+const getColorScheme = (theme: Theme): string => {
+ return LIGHT_THEMES.has(theme) ? 'light' : 'dark';
+};
+
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('system');
const [mounted, setMounted] = useState(false);
@@ -36,7 +42,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const effectiveTheme = getEffectiveTheme(initialTheme);
document.documentElement.classList.remove(...THEMES);
document.documentElement.classList.add(effectiveTheme);
- document.documentElement.style.colorScheme = effectiveTheme;
+ document.documentElement.style.colorScheme = getColorScheme(effectiveTheme);
if (!stored) {
localStorage.setItem('theme', initialTheme);
}
@@ -49,8 +55,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
root.classList.remove(...THEMES);
root.classList.add(effectiveTheme);
- root.style.colorScheme = effectiveTheme;
-
+ root.style.colorScheme = getColorScheme(effectiveTheme);
+
localStorage.setItem('theme', newTheme);
setTheme(newTheme);
};
@@ -64,7 +70,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
const root = window.document.documentElement;
root.classList.remove(...THEMES);
root.classList.add(effectiveTheme);
- root.style.colorScheme = effectiveTheme;
+ root.style.colorScheme = getColorScheme(effectiveTheme);
}
};
diff --git a/tests/delete.spec.ts b/tests/delete.spec.ts
index 8de7db8..6d8004b 100644
--- a/tests/delete.spec.ts
+++ b/tests/delete.spec.ts
@@ -28,6 +28,13 @@ test.describe('Document deletion flow', () => {
});
test('deletes all local documents from Settings modal', async ({ page }) => {
+ // This test only applies when auth is NOT enabled, since with auth
+ // the bulk-delete UI lives in the delete-account flow instead.
+ test.skip(
+ Boolean(process.env.AUTH_SECRET && process.env.BASE_URL),
+ 'Bulk document deletion is part of the delete-account flow when auth is enabled',
+ );
+
// Upload multiple docs (PDF + EPUB)
await uploadFile(page, 'sample.pdf');
await uploadFile(page, 'sample.epub');
diff --git a/tests/helpers.ts b/tests/helpers.ts
index 36e34ff..8cdba36 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -346,18 +346,18 @@ export async function deleteDocumentByName(page: Page, fileName: string) {
await confirmBtn.click();
}
-// Open Settings modal and navigate to Documents tab
+// Open Settings modal and navigate to Documents section
export async function openSettingsDocumentsTab(page: Page) {
await page.getByRole('button', { name: 'Settings' }).click();
- await page.getByRole('tab', { name: '📄 Docs' }).click();
+ await page.getByRole('button', { name: 'Documents' }).click();
}
// Delete all local documents through Settings and close dialogs
export async function deleteAllLocalDocuments(page: Page) {
await openSettingsDocumentsTab(page);
- await page.getByRole('button', { name: /Delete (anonymous docs|all user docs|server docs)/i }).click();
+ await page.getByRole('button', { name: /Delete all data/i }).click();
- const heading = page.getByRole('heading', { name: /Delete (Anonymous Docs|All User Docs|Server Docs)/i });
+ const heading = page.getByRole('heading', { name: /Delete All Data/i });
await expect(heading).toBeVisible({ timeout: 10000 });
const confirmBtn = heading.locator('xpath=ancestor::*[@role="dialog"][1]//button[normalize-space()="Delete"]');