[eric] workflows+dashboard: card width matches chat, Cmd/Ctrl+A select-all, compact scheduling + consistent cancel/title

This commit is contained in:
ciregenz
2026-05-25 12:45:59 -07:00
parent 846ee3bb01
commit c5d2fe512f
5 changed files with 38 additions and 11 deletions
@@ -91,6 +91,21 @@ export function useDashboardShortcuts({
return () => window.removeEventListener('keydown', handleDelete);
}, [selection, dispatch]);
// Cmd/Ctrl+A selects every card so it can be deleted in one go. Skipped
// inside text fields so Cmd+A there still selects text, not cards.
useEffect(() => {
const handleSelectAll = (e: KeyboardEvent) => {
if (!isActive) return;
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'a') return;
const tag = (e.target as HTMLElement)?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
e.preventDefault();
selection.selectAll();
};
window.addEventListener('keydown', handleSelectAll);
return () => window.removeEventListener('keydown', handleSelectAll);
}, [selection, isActive]);
// Cmd+F to open card search palette
useEffect(() => {
const handleSearch = (e: KeyboardEvent) => {
@@ -70,6 +70,18 @@ export function useDashboardSelection(
const deselectAll = useCallback(() => setSelectedIds(new Map()), []);
// Cmd/Ctrl+A: select every card on the canvas so the user can wipe the
// board in one keystroke. Mirrors the per-type id keys the marquee uses.
const selectAll = useCallback(() => {
const next = new Map<string, CardType>();
for (const card of Object.values(cards)) next.set(card.session_id, 'agent');
for (const vc of Object.values(viewCards)) next.set(vc.output_id, 'view');
for (const bc of Object.values(browserCards)) next.set(bc.browser_id, 'browser');
for (const n of Object.values(notes)) next.set(n.note_id, 'note');
for (const wc of Object.values(workflowCards)) next.set(wc.workflow_id, 'workflow');
setSelectedIds(next);
}, [cards, viewCards, browserCards, notes, workflowCards]);
const selectCard = useCallback(
(id: string, type: CardType, shiftKey: boolean) => {
setSelectedIds((prev) => {
@@ -285,6 +297,7 @@ export function useDashboardSelection(
isSelected,
selectCard,
deselectAll,
selectAll,
handleCanvasMouseDown,
handleCanvasMouseMove,
handleCanvasMouseUp,
@@ -133,9 +133,10 @@ export default function SchedulingView({ workflow, steps }: Props) {
}, [pending, dispatch, workflow.id, workflow.updated_at]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25 }}>
{/* Inline header replacement. Image #49: subtitle on LEFT, Cancel
on RIGHT. The default History/Run row is hidden for 'scheduling'. */}
on RIGHT. Cancel matches the subtitle's weight/size/color so the
row reads as peers, not a heavy CTA; it just reddens on hover. */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<InlineSubtitle workflow={workflow} />
<Box sx={{ flex: 1 }} />
@@ -143,13 +144,12 @@ export default function SchedulingView({ workflow, steps }: Props) {
onClick={onCancel}
role="button"
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.45,
fontSize: '0.82rem', fontWeight: 600,
color: c.text.secondary, cursor: 'pointer',
px: 0.75, py: 0.5,
display: 'inline-flex', alignItems: 'center', gap: 0.4,
fontSize: '0.82rem', fontWeight: 500,
color: c.text.muted, cursor: 'pointer',
'&:hover': { color: c.status.error },
}}>
<DeleteOutlineRounded sx={{ fontSize: 16 }} />
<DeleteOutlineRounded sx={{ fontSize: 15 }} />
Cancel task scheduling
</Box>
</Box>
@@ -160,7 +160,6 @@ export default function SchedulingView({ workflow, steps }: Props) {
{error && (
<Typography sx={{ fontSize: '0.82rem', color: c.status.error }}>{error}</Typography>
)}
<Box sx={{ flex: 1 }} />
{/* Real ChatInput (same one the toolbar / agent chat use) so the
composer matches Image #54 / #64 exactly: live model picker,
mode picker, thinking level, paperclip + mic, the works. We
@@ -463,14 +463,14 @@ const WorkflowCard: React.FC<Props> = ({
placeholder="New workflow"
onChange={(e) => dispatch(updateWorkflowCard({ workflowId, patch: { draft: { ...(card?.draft || {}), title: e.target.value } } }))}
sx={{
flex: 1, fontWeight: 700, fontSize: '1rem', color: c.text.primary,
flex: 1, fontWeight: 600, fontSize: '0.95rem', color: c.text.primary,
letterSpacing: '-0.005em',
'& input::placeholder': { color: c.text.muted, opacity: 1 },
}}
/>
) : (
<>
<Typography sx={{ fontWeight: 700, fontSize: '1rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
<Typography sx={{ fontWeight: 600, fontSize: '0.95rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
{title}
</Typography>
<StatusPill view={card.view} workflow={workflow} runs={runs} />
@@ -18,7 +18,7 @@ export const DEFAULT_VIEW_CARD_W = 1280;
export const DEFAULT_VIEW_CARD_H = 800;
export const DEFAULT_BROWSER_CARD_W = 1280;
export const DEFAULT_BROWSER_CARD_H = 800;
export const DEFAULT_WORKFLOW_CARD_W = 440;
export const DEFAULT_WORKFLOW_CARD_W = 480;
export const DEFAULT_WORKFLOW_CARD_H = 520;
export const DEFAULT_WORKFLOWS_HUB_W = 1200;
export const DEFAULT_WORKFLOWS_HUB_H = 640;