diff --git a/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx b/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx
index 73772dfa..953313d6 100644
--- a/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router';
import { useQuery, useQueryClient } from '@tanstack/react-query';
@@ -344,7 +345,8 @@ export const CapabilityList = ({
queryFn: () => client.get(endpoint),
});
- const handleCreate = async () => {
+ const handleCreate = async (ev?: FormEvent) => {
+ ev?.preventDefault();
const name = newName.trim();
if (!name) return;
try {
@@ -382,16 +384,15 @@ export const CapabilityList = ({
)}
{creating && (
-
+
+
)}
{externalSearch === undefined && (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx
index e9a43ed3..95534cb9 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -61,7 +62,8 @@ export const ApiKeys = () => {
const refresh = () => queryClient.invalidateQueries({ queryKey: API_KEYS_QUERY_KEY });
- const create = async () => {
+ const create = async (ev?: FormEvent) => {
+ ev?.preventDefault();
const trimmed = name.trim();
if (!trimmed || isCreating) return;
setIsCreating(true);
@@ -130,22 +132,21 @@ export const ApiKeys = () => {
)}
-
+
{keys.length === 0 ? (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx
index ad4d3eb7..fb82ebb4 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Input } from '@/components/ui/input';
@@ -31,7 +32,8 @@ export const ApifyConfig = () => {
.finally(() => setIsLoading(false));
}, []);
- const handleSave = async () => {
+ const handleSave = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -60,7 +62,7 @@ export const ApifyConfig = () => {
)}
-
+
+
);
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx
index edbb83fc..98ccd0c5 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { Copy, Trash2, Ban, Plus, Smartphone } from 'lucide-react';
@@ -59,7 +60,8 @@ export const DavAppPasswords = () => {
useEffect(load, [load]);
- const create = async () => {
+ const create = async (ev?: FormEvent) => {
+ ev?.preventDefault();
const trimmed = label.trim();
if (!trimmed || isCreating) return;
setIsCreating(true);
@@ -142,22 +144,21 @@ export const DavAppPasswords = () => {
)}
-
+
{passwords.length === 0 ? (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx
index 96452d52..f6beaa96 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { ChevronDown } from 'lucide-react';
@@ -266,7 +267,8 @@ export const GoogleOAuthConfig = () => {
if (!isLoading && clientId && clientSecret) verify();
}, [isLoading]);
- const handleSave = async () => {
+ const handleSave = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -290,7 +292,7 @@ export const GoogleOAuthConfig = () => {
-
+
+
);
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx
index d155b151..586bec8b 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx
@@ -226,6 +226,23 @@ export const AIHarnessesSection = () => {
}
};
+ const submitLocalUrl = (ev?: React.FormEvent) => {
+ ev?.preventDefault();
+ if (probeState.step === 'url' && localUrl.trim()) handleProbe();
+ };
+
+ const submitAuth = (ev?: React.FormEvent) => {
+ ev?.preventDefault();
+ if (probeState.step !== 'auth') return;
+ const ready = probeState.probe.authType === 'basic' ? authUsername && authPassword : authApiKey;
+ if (ready) handleAuthSubmit(probeState.probe);
+ };
+
+ const submitApiKey = (ev: React.FormEvent, providerId: string) => {
+ ev.preventDefault();
+ if (keyInputs[providerId]) saveApiKey(providerId);
+ };
+
const removeLocalProvider = async (id: string) => {
await client.delete(`/server-settings/chat-providers/local-providers/${id}`);
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] });
@@ -291,57 +308,60 @@ export const AIHarnessesSection = () => {
{addingLocal ? (
{/* Step 1: Name + URL inputs stacked */}
-
setLocalName(ev.target.value)}
- onKeyDown={(ev) => {
- if (ev.key === 'Escape') resetLocalForm();
- }}
- disabled={probeState.step !== 'url'}
- autoFocus
- />
-
setLocalUrl(ev.target.value)}
- onKeyDown={(ev) => {
- if (ev.key === 'Escape') resetLocalForm();
- if (ev.key === 'Enter' && localUrl.trim() && probeState.step === 'url') handleProbe();
- }}
- disabled={probeState.step !== 'url'}
- />
-
+
{/* Step 2: Auth form (if needed) */}
{probeState.step === 'auth' && (
-
+
+
)}
) : (
@@ -460,7 +471,10 @@ export const AIHarnessesSection = () => {
{editingProvider === provider.key && (
-
+
+
)}
))}
@@ -501,7 +513,11 @@ export const AIHarnessesSection = () => {
if (!provider || connectedProviders.some((cp) => cp.providerId === provider.providerId))
return null;
return (
-
+
+
);
})()}
{unconnectedProviders.length > 0 && (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx
index aa0360ed..440f61a6 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -51,7 +52,8 @@ export const OCRSection = () => {
fetchModels(config.url);
}, [config]);
- const handleSave = async () => {
+ const handleSave = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -92,7 +94,7 @@ export const OCRSection = () => {
if (isLoading) return Loading...
;
return (
-
+
);
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx
index 97648d43..881b8cdd 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -84,7 +85,8 @@ export const SMTPSection = () => {
...(provider === 'resend' ? { apiKey } : { host, port: parseInt(port) || 587, username, password, secure }),
});
- const handleSave = async () => {
+ const handleSave = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -118,7 +120,8 @@ export const SMTPSection = () => {
}
};
- const handleTest = async () => {
+ const handleTest = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isTesting || !testEmail) return;
setIsTesting(true);
try {
@@ -151,150 +154,147 @@ export const SMTPSection = () => {
return (
-
- Provider
- handleProviderChange(v as Provider)}>
-
-
-
-
- MailHog (local testing)
- Resend
- Custom SMTP
-
-
-
-
- {provider === 'resend' && (
+
);
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx
index 68ef5a60..1333ad49 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -28,7 +29,8 @@ export const STTSection = () => {
setUrl(config.url);
}, [config]);
- const handleSave = async () => {
+ const handleSave = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -69,7 +71,7 @@ export const STTSection = () => {
if (isLoading) return Loading...
;
return (
-
+
);
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx
index 3e5f9f07..39d0514e 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -102,7 +103,8 @@ export const TTSSection = () => {
...(apiKey ? { apiKey } : {}),
});
- const handleSave = async () => {
+ const handleSave = async (ev?: FormEvent) => {
+ ev?.preventDefault();
if (isSaving) return;
setIsSaving(true);
try {
@@ -152,7 +154,7 @@ export const TTSSection = () => {
if (isLoading) return Loading...
;
return (
-
+
);
};
diff --git a/src/workspaces/officerdev/src/apps/AppStore/AppStoreDetail.tsx b/src/workspaces/officerdev/src/apps/AppStore/AppStoreDetail.tsx
index 4a9c748e..3c8f7b34 100644
--- a/src/workspaces/officerdev/src/apps/AppStore/AppStoreDetail.tsx
+++ b/src/workspaces/officerdev/src/apps/AppStore/AppStoreDetail.tsx
@@ -60,7 +60,8 @@ export const AppStoreDetail = () => {
const fields = chosen === 'existing' ? (item.existingFields ?? []) : (item.configFields ?? []);
const isInstalled = item.install.status === 'installed' || item.install.status === 'blocked';
- const run = async (replace = false) => {
+ const run = async (ev?: React.FormEvent, replace = false) => {
+ ev?.preventDefault();
setLog([]);
const result = await install.mutateAsync({
id: item.id,
@@ -118,7 +119,8 @@ export const AppStoreDetail = () => {
) : (
-
+ // A real
)}
{/* The install log. Streaming it into a terminal panel is the intended shape; until then it is
diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx
index 6fcbda95..49caa94a 100644
--- a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx
+++ b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx
@@ -89,24 +89,31 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => {
renaming === s.id ? (
// Inline, because the alternative is a modal over a panel header — and the only thing being
// asked for is one line of text.
-
ev.stopPropagation()}>
+
+
) : (
0 : body.trim().length > 0;
- const submit = () => {
+ const submit = (ev?: FormEvent) => {
+ ev?.preventDefault();
if (!canSubmit) return;
const spec: BackgroundJobSpec = {
mode,
@@ -123,7 +125,7 @@ export const BackgroundJobDialog = ({
-
-
- onOpenChange(false)}>
- Cancel
-
-
- Start
-
-
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Start
+
+
+
);
diff --git a/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx b/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx
index 38a57a45..f17def64 100644
--- a/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx
+++ b/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx
@@ -163,31 +163,29 @@ export const QuestionActivity = ({ message, onAnswer }: QuestionActivityProps) =
{/* "Other" — provided by the client on purpose. Both schemas say the model must NOT offer one
as an option, precisely because the escape hatch is the UI's job. */}
{!done && (
-
+
+
)}
{multi && !done && (
diff --git a/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx b/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx
index ce2bbfc7..9eccfd11 100644
--- a/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx
+++ b/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx
@@ -1,3 +1,4 @@
+import type { FormEvent } from 'react';
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
@@ -11,7 +12,8 @@ type WebpageDialogProps = {
export function WebpageDialog({ open, onOpenChange, onSubmit }: WebpageDialogProps) {
const [urlInput, setUrlInput] = useState('');
- const handleSubmit = () => {
+ const handleSubmit = (ev?: FormEvent) => {
+ ev?.preventDefault();
const url = urlInput.trim();
if (!url) return;
onSubmit(url);
@@ -26,29 +28,23 @@ export function WebpageDialog({ open, onOpenChange, onSubmit }: WebpageDialogPro
Attach Webpage
Enter a URL to scrape and attach as context.
-
+
+
);
diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx
index 01b92e60..18004c52 100644
--- a/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx
+++ b/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx
@@ -40,7 +40,8 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
const goto = (idx: number) => setPath(idx < 0 ? '/' : `/${segments.slice(0, idx + 1).join('/')}`);
const enter = (name: string) => setPath(path === '/' ? `/${name}` : `${path}/${name}`);
- const createFolder = async () => {
+ const createFolder = async (ev?: React.FormEvent) => {
+ ev?.preventDefault();
const n = newName.trim();
if (!n) return;
await api.createDir(path === '/' ? `/${n}` : `${path}/${n}`);
@@ -104,25 +105,24 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps)
{/* Footer */}
{creating ? (
-
+
+
) : (
+
+
);
}
diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx
index 93285ded..7e98d3e5 100644
--- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx
+++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx
@@ -38,7 +38,8 @@ export const CommandHqCategories = () => {
const [adding, setAdding] = useState(false);
const [name, setName] = useState('');
- const submitNew = async () => {
+ const submitNew = async (ev?: React.FormEvent) => {
+ ev?.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setAdding(false);
@@ -92,21 +93,23 @@ export const CommandHqCategories = () => {
/>
))}
{adding && (
-
setName(ev.target.value)}
- onBlur={submitNew}
- onKeyDown={(ev) => {
- if (ev.key === 'Enter') submitNew();
- if (ev.key === 'Escape') {
- setName('');
- setAdding(false);
- }
- }}
- placeholder="Category name"
- className="mx-1 rounded-lg border border-border bg-background px-2 py-1.5 text-sm outline-none focus:border-primary"
- />
+ // No submit button — a form with exactly one text field still submits on Enter.
+
)}
{!adding && categories.length === 0 && (
No categories yet
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx
index 80572b80..d51e5195 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx
@@ -503,22 +503,31 @@ const InlineRenameInput = ({
};
return (
-
setValue(ev.target.value)}
- onBlur={() => {
- // Ignore blur before the input is ready (context menu closing steals focus)
- if (!ready.current) return;
+ // A real
);
};
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx
index f6b0b14f..973307bd 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx
@@ -104,7 +104,8 @@ export const NewEntryDialog = ({ kind, onClose, onSubmit, existingNames }: NewEn
const problem = kind === 'clone' ? urlProblem(name) : nameProblem(name, existingNames);
const canSubmit = name.trim().length > 0 && problem === null;
- const commit = () => {
+ const commit = (ev?: React.FormEvent) => {
+ ev?.preventDefault();
if (!canSubmit) return;
onSubmit(name.trim());
onClose();
@@ -113,42 +114,41 @@ export const NewEntryDialog = ({ kind, onClose, onSubmit, existingNames }: NewEn
return (
!open && onClose()}>
-
-
-
- {title}
-
- {description}
-
+ {/* A real
);
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx
index 05c38c84..b0089d6e 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx
@@ -157,7 +157,8 @@ const AgenticTaskRunner = ({
wasGenerating.current = chat.isGenerating;
}, [chat.isGenerating]);
- const handleRun = () => {
+ const handleRun = (ev?: React.FormEvent) => {
+ ev?.preventDefault();
// Build prompt from task body + input values, or fall back to generic prompt
let prompt = defaultInput;
if (taskBody && inputDefs) {
@@ -182,24 +183,29 @@ const AgenticTaskRunner = ({
if (phase === 'ready') {
return (
<>
- {hasConfigurableInputs && (
-
- )}
-
+ {/* A real
+ {/* type="button": this form's toggles must never submit it, since they now live inside a
+ real
);
}
diff --git a/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx b/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx
index 82f4958a..79a51faf 100644
--- a/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx
+++ b/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx
@@ -26,7 +26,8 @@ export const PreviewHeader = ({ panelId }: { panelId?: string }) => {
if (!editing) setDraft(display);
}, [display, editing]);
- const commit = () => {
+ const commit = (ev?: React.FormEvent) => {
+ ev?.preventDefault();
const next = normalizeUrl(draft);
if (next) setUrl(next);
setEditing(false);
@@ -34,25 +35,29 @@ export const PreviewHeader = ({ panelId }: { panelId?: string }) => {
if (editing) {
return (
- <>
+
setDraft(ev.target.value)}
onKeyDown={(ev) => {
- if (ev.key === 'Enter') commit();
if (ev.key === 'Escape') setEditing(false);
}}
className="min-w-0 flex-1 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-xs"
/>
-
+
- setEditing(false)} title="Cancel" className="p-0.5 cursor-pointer shrink-0">
+ setEditing(false)}
+ title="Cancel"
+ className="p-0.5 cursor-pointer shrink-0"
+ >
- >
+
);
}
diff --git a/src/workspaces/widgets/DailyGoals/index.tsx b/src/workspaces/widgets/DailyGoals/index.tsx
index 747b96dd..65dec99f 100644
--- a/src/workspaces/widgets/DailyGoals/index.tsx
+++ b/src/workspaces/widgets/DailyGoals/index.tsx
@@ -85,7 +85,8 @@ export const DailyGoals = () => {
const [newGoal, setNewGoal] = useState('');
const inputRef = useRef
(null);
- const addGoal = () => {
+ const addGoal = (ev?: React.FormEvent) => {
+ ev?.preventDefault();
const text = newGoal.trim();
if (!text) return;
nextGoalId++;
@@ -131,13 +132,22 @@ export const DailyGoals = () => {
toggleGoal(goal.id)}
>
{goal.done && (
-
+
)}
@@ -162,29 +172,22 @@ export const DailyGoals = () => {
)}
-
+
);
diff --git a/src/workspaces/widgets/Pomodoro/index.tsx b/src/workspaces/widgets/Pomodoro/index.tsx
index 0966c9e8..812aa857 100644
--- a/src/workspaces/widgets/Pomodoro/index.tsx
+++ b/src/workspaces/widgets/Pomodoro/index.tsx
@@ -79,14 +79,14 @@ function usePomodoroState() {
type Phase = 'focus' | 'break' | 'long-break';
const phaseLabel: Record = {
- 'focus': 'Focus',
- 'break': 'Break',
+ focus: 'Focus',
+ break: 'Break',
'long-break': 'Long Break',
};
const phaseColor: Record = {
- 'focus': 'text-destructive',
- 'break': 'text-success',
+ focus: 'text-destructive',
+ break: 'text-success',
'long-break': 'text-accent',
};
@@ -116,7 +116,8 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => {
const audioRef = useRef(null);
const filtered = useMemo(() => {
- const source = activeCategory === 'all' ? allSounds : (categories.find((c) => c.id === activeCategory)?.sounds ?? []);
+ const source =
+ activeCategory === 'all' ? allSounds : (categories.find((c) => c.id === activeCategory)?.sounds ?? []);
if (!search) return source;
const q = search.toLowerCase();
return source.filter((s) => s.name.toLowerCase().includes(q));
@@ -165,7 +166,10 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => {
{ stopPreview(); setOpen(false); }}
+ onClick={() => {
+ stopPreview();
+ setOpen(false);
+ }}
>
Done
@@ -175,6 +179,11 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => {
placeholder="Search sounds..."
value={search}
onChange={(ev) => setSearch(ev.target.value)}
+ // This is a filter, not a submit — but it now lives inside SettingsPanel's , so without
+ // this it would submit the settings on Enter instead of just narrowing the list.
+ onKeyDown={(ev) => {
+ if (ev.key === 'Enter') ev.preventDefault();
+ }}
className="w-full rounded border border-border bg-background px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
/>
@@ -199,23 +208,31 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => {
className={`flex items-center gap-1.5 rounded px-2 py-1 text-left text-xs cursor-pointer transition-colors ${
value === null ? 'bg-primary/10 text-primary' : 'hover:bg-accent'
}`}
- onClick={() => { stopPreview(); onChange(null); }}
+ onClick={() => {
+ stopPreview();
+ onChange(null);
+ }}
>
None (silent)
{filtered.map((s) => (
-
+
previewName === s.name ? stopPreview() : preview(s)}
+ onClick={() => (previewName === s.name ? stopPreview() : preview(s))}
>
{previewName === s.name ? : }
{ onChange(s.name); }}
+ onClick={() => {
+ onChange(s.name);
+ }}
>
{formatSoundName(s.name)}
@@ -229,15 +246,23 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => {
const SettingsPanel = ({ settings, onSave }: { settings: PomodoroSettings; onSave: (s: PomodoroSettings) => void }) => {
const [draft, setDraft] = useState(settings);
- const numericFields: { key: 'focusMinutes' | 'breakMinutes' | 'longBreakMinutes' | 'sessionsBeforeLong'; label: string }[] = [
+ const numericFields: {
+ key: 'focusMinutes' | 'breakMinutes' | 'longBreakMinutes' | 'sessionsBeforeLong';
+ label: string;
+ }[] = [
{ key: 'focusMinutes', label: 'Focus (min)' },
{ key: 'breakMinutes', label: 'Break (min)' },
{ key: 'longBreakMinutes', label: 'Long break (min)' },
{ key: 'sessionsBeforeLong', label: 'Sessions before long' },
];
+ const save = (ev: React.FormEvent) => {
+ ev.preventDefault();
+ onSave(draft);
+ };
+
return (
-
+
{numericFields.map(({ key, label }) => (
{label}
@@ -262,14 +287,13 @@ const SettingsPanel = ({ settings, onSave }: { settings: PomodoroSettings; onSav
onChange={(name) => setDraft((d) => ({ ...d, breakEndSound: name }))}
/>
onSave(draft)}
>
Save
-
+
);
};
@@ -363,7 +387,15 @@ export const Pomodoro = () => {
-
+
{
- {completedToday} session{completedToday !== 1 ? 's' : ''} today
+
+ {completedToday} session{completedToday !== 1 ? 's' : ''} today
+