Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 23 additions & 58 deletions web/src/components/ShortcutsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import { X } from 'lucide-react';
import { useUIStore } from '../stores/uiStore';
import { formatCombo, type ShortcutCombo } from '../utils/keyboard';
import { Modal } from './ui/Modal';

interface DisplayShortcut {
combo: ShortcutCombo;
Expand Down Expand Up @@ -51,65 +50,31 @@ export function ShortcutsModal() {
const open = useUIStore((s) => s.shortcutsModalOpen);
const close = useUIStore((s) => s.closeShortcutsModal);

// Local Esc handler — runs *before* the document-level shortcut listeners
// because modal mount captures it first when focus is inside.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
close();
}
};
document.addEventListener('keydown', onKey, true); // capture phase = wins
return () => document.removeEventListener('keydown', onKey, true);
}, [open, close]);

if (!open) return null;

// The capture-phase Escape handling this component used to own is now
// the Modal's job, along with the focus trap it never had.
return (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
onClick={close}
>
<div
className="bg-surface-raised border border-border-subtle rounded-xl w-[520px] max-w-[90vw] max-h-[80vh] flex flex-col"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between px-5 py-3 border-b border-border">
<h2 className="text-[15px] font-semibold">Keyboard shortcuts</h2>
<button
onClick={close}
className="text-text-faint hover:text-text-muted cursor-pointer p-1"
title="Close"
>
<X size={18} />
</button>
</div>

<div className="overflow-y-auto p-5 space-y-5">
{SECTIONS.map((section) => (
<div key={section.title}>
<h3 className="text-[11px] uppercase tracking-wider text-text-faint font-medium mb-2">
{section.title}
</h3>
<div className="space-y-1.5">
{section.items.map((item, idx) => (
<div
key={idx}
className="flex items-center justify-between gap-4 py-1"
>
<span className="text-[13px] text-text-secondary">{item.description}</span>
<Kbd combo={item.combo} />
</div>
))}
</div>
<Modal open={open} onClose={close} title="Keyboard shortcuts" size="lg">
<div className="p-5 space-y-5">
{SECTIONS.map((section) => (
<div key={section.title}>
<h3 className="text-[11px] uppercase tracking-wider text-text-faint font-medium mb-2">
{section.title}
</h3>
<div className="space-y-1.5">
{section.items.map((item, idx) => (
<div
key={idx}
className="flex items-center justify-between gap-4 py-1"
>
<span className="text-[13px] text-text-secondary">{item.description}</span>
<Kbd combo={item.combo} />
</div>
))}
</div>
))}
</div>
</div>
))}
</div>
</div>
</Modal>
);
}

Expand Down
108 changes: 59 additions & 49 deletions web/src/components/Tasks/TaskCreateDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState } from 'react';
import { X } from 'lucide-react';
import { Modal } from '../ui/Modal';

const FORM_ID = 'task-create-form';

Comment thread
alex-clickhouse marked this conversation as resolved.
export function TaskCreateDialog({ onClose, onCreate }: {
onClose: () => void;
Expand All @@ -16,55 +18,63 @@ export function TaskCreateDialog({ onClose, onCreate }: {
};

return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-surface-raised border border-border-subtle rounded-xl w-[480px] max-w-[90vw]" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-3 border-b border-border">
<h2 className="text-[15px] font-semibold">New Task</h2>
<button onClick={onClose} className="text-text-faint hover:text-text-muted cursor-pointer p-1">
<X size={18} />
<Modal
open
onClose={onClose}
title="New Task"
// A stray backdrop click shouldn't bin a half-typed task.
closeOnBackdrop={false}
footer={
<>
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-[13px] text-text-muted hover:text-text-secondary cursor-pointer"
>
Cancel
</button>
{/* Outside the <form>, associated by id — keeps the button in the
modal's footer slot while Enter-to-submit still works. */}
<button
type="submit"
form={FORM_ID}
className="px-4 py-2 text-[13px] bg-accent hover:bg-accent-hover text-white rounded-lg cursor-pointer disabled:opacity-50"
disabled={!title.trim()}
>
Create
</button>
</>
}
>
<form id={FORM_ID} onSubmit={handleSubmit} className="p-5 space-y-4">
<div>
<label className="block text-[12px] text-text-muted mb-1">Title</label>
<input
value={title}
onChange={e => setTitle(e.target.value)}
autoFocus
className="w-full px-3 py-2 bg-surface-raised border border-border-subtle rounded-lg text-[14px] text-text outline-none focus:border-accent/50"
/>
</div>
<div>
<label className="block text-[12px] text-text-muted mb-1">Details</label>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
rows={4}
className="w-full px-3 py-2 bg-surface-raised border border-border-subtle rounded-lg text-[14px] text-text outline-none focus:border-accent/50 resize-none"
/>
</div>
<div>
<label className="block text-[12px] text-text-muted mb-1">Deadline</label>
<input
type="date"
value={deadline}
onChange={e => setDeadline(e.target.value)}
className="px-3 py-2 bg-surface-raised border border-border-subtle rounded-lg text-[14px] text-text outline-none focus:border-accent/50"
/>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4">
<div>
<label className="block text-[12px] text-text-muted mb-1">Title</label>
<input
value={title}
onChange={e => setTitle(e.target.value)}
autoFocus
className="w-full px-3 py-2 bg-surface-raised border border-border-subtle rounded-lg text-[14px] text-text outline-none focus:border-accent/50"
/>
</div>
<div>
<label className="block text-[12px] text-text-muted mb-1">Details</label>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
rows={4}
className="w-full px-3 py-2 bg-surface-raised border border-border-subtle rounded-lg text-[14px] text-text outline-none focus:border-accent/50 resize-none"
/>
</div>
<div>
<label className="block text-[12px] text-text-muted mb-1">Deadline</label>
<input
type="date"
value={deadline}
onChange={e => setDeadline(e.target.value)}
className="px-3 py-2 bg-surface-raised border border-border-subtle rounded-lg text-[14px] text-text outline-none focus:border-accent/50"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button type="button" onClick={onClose}
className="px-4 py-2 text-[13px] text-text-muted hover:text-text-muted cursor-pointer">
Cancel
</button>
<button type="submit"
className="px-4 py-2 text-[13px] bg-accent hover:bg-accent-hover text-white rounded-lg cursor-pointer disabled:opacity-50"
disabled={!title.trim()}>
Create
</button>
</div>
</form>
</div>
</div>
</form>
</Modal>
);
}
155 changes: 76 additions & 79 deletions web/src/components/Tasks/TaskStatusManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
statusBadgeStyle,
type TaskStatusDef,
} from '../../stores/taskStatusStore';
import { Modal } from '../ui/Modal';

const PALETTE = [
'#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e',
Expand Down Expand Up @@ -100,26 +101,86 @@ export function TaskStatusManager({ onClose }: { onClose: () => void }) {
finally { setBusy(false); }
};

return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={onClose}>
<div
className="bg-surface-raised border border-border-subtle rounded-xl w-[560px] max-w-[92vw] max-h-[85vh] flex flex-col"
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between px-5 py-3 border-b border-border">
<h2 className="text-[15px] font-semibold">Task Statuses</h2>
<button onClick={onClose} className="text-text-faint hover:text-text-muted cursor-pointer p-1">
<X size={18} />
</button>
</div>
// The add form is pinned below the scrolling status list, so it stays
// reachable however many statuses are configured. It's a form rather
// than a button row, hence the footer layout override.
const addStatusFooter = showAdd ? (
<div className="space-y-3">
<div className="flex gap-2">
<label
className="relative w-9 h-9 rounded-lg border border-border shrink-0 cursor-pointer"
style={{ backgroundColor: color }}
title="Pick a color"
>
<input
type="color"
value={color}
onChange={e => setColor(e.target.value)}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
</label>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="name (e.g. in_review)"
autoFocus
className="flex-1 px-3 py-2 bg-surface border border-border-subtle rounded-lg text-[13px] text-text outline-none focus:border-accent/50"
/>
<input
value={label}
onChange={e => setLabel(e.target.value)}
placeholder="Label (optional)"
className="flex-1 px-3 py-2 bg-surface border border-border-subtle rounded-lg text-[13px] text-text outline-none focus:border-accent/50"
/>
</div>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={2}
placeholder="Description (optional)"
className="w-full px-3 py-2 bg-surface border border-border-subtle rounded-lg text-[13px] text-text outline-none focus:border-accent/50 resize-none"
/>
<div className="flex justify-end gap-2">
<button
onClick={resetAdd}
className="px-3 py-1.5 text-[13px] text-text-muted hover:text-text cursor-pointer"
>
Cancel
</button>
<button
onClick={handleCreate}
disabled={busy || !name.trim()}
className="flex items-center gap-1.5 px-3 py-1.5 text-[13px] bg-accent hover:bg-accent-hover text-white rounded-lg cursor-pointer disabled:opacity-50"
>
<Plus size={14} /> Add status
</button>
</div>
</div>
) : (
<button
onClick={() => { setColor(randomColor()); setShowAdd(true); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-[13px] text-accent hover:bg-accent/10 rounded-lg cursor-pointer"
>
<Plus size={14} /> New status
</button>
);

return (
<Modal
open
onClose={onClose}
title="Task Statuses"
size="xl"
footer={addStatusFooter}
footerClassName="p-4"
>
<div className="p-5 space-y-2">
{error && (
<div className="mx-5 mt-3 px-3 py-2 text-[12px] text-hue-red bg-red-400/10 border border-red-400/20 rounded-lg">
<div className="px-3 py-2 mb-1 text-[12px] text-hue-red bg-red-400/10 border border-red-400/20 rounded-lg">
{error}
</div>
)}

<div className="flex-1 overflow-y-auto p-5 space-y-2">
{statuses.map(s => (
<div key={s.name} className="border border-border-subtle rounded-lg p-3">
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -215,71 +276,7 @@ export function TaskStatusManager({ onClose }: { onClose: () => void }) {
)}
</div>
))}
</div>

<div className="border-t border-border p-4">
{showAdd ? (
<div className="space-y-3">
<div className="flex gap-2">
<label
className="relative w-9 h-9 rounded-lg border border-border shrink-0 cursor-pointer"
style={{ backgroundColor: color }}
title="Pick a color"
>
<input
type="color"
value={color}
onChange={e => setColor(e.target.value)}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
</label>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="name (e.g. in_review)"
autoFocus
className="flex-1 px-3 py-2 bg-surface border border-border-subtle rounded-lg text-[13px] text-text outline-none focus:border-accent/50"
/>
<input
value={label}
onChange={e => setLabel(e.target.value)}
placeholder="Label (optional)"
className="flex-1 px-3 py-2 bg-surface border border-border-subtle rounded-lg text-[13px] text-text outline-none focus:border-accent/50"
/>
</div>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={2}
placeholder="Description (optional)"
className="w-full px-3 py-2 bg-surface border border-border-subtle rounded-lg text-[13px] text-text outline-none focus:border-accent/50 resize-none"
/>
<div className="flex justify-end gap-2">
<button
onClick={resetAdd}
className="px-3 py-1.5 text-[13px] text-text-muted hover:text-text cursor-pointer"
>
Cancel
</button>
<button
onClick={handleCreate}
disabled={busy || !name.trim()}
className="flex items-center gap-1.5 px-3 py-1.5 text-[13px] bg-accent hover:bg-accent-hover text-white rounded-lg cursor-pointer disabled:opacity-50"
>
<Plus size={14} /> Add status
</button>
</div>
</div>
) : (
<button
onClick={() => { setColor(randomColor()); setShowAdd(true); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-[13px] text-accent hover:bg-accent/10 rounded-lg cursor-pointer"
>
<Plus size={14} /> New status
</button>
)}
</div>
</div>
</div>
</Modal>
);
}
Loading
Loading