refactor: update Timeline dialogs to match application design system

Replaces Material-UI components in Timeline dialogs with custom Tailwind
CSS styling to match the application's standard dialog design.

Changes:
- RenameStateDialog: Replaced MUI Dialog, TextField, and Button components
  with custom styled elements matching ConfirmDialog/InputDialog pattern
- CreateStateDialog: Converted all MUI form components (TextField, Checkbox,
  Button) to native HTML elements with Tailwind styling
- Added consistent visual design with large icons, gray backgrounds, and
  proper spacing
- Improved keyboard handling (Enter/Escape) with proper event listeners
- Enhanced focus management with auto-select on input fields
- Maintained all existing functionality while improving UI consistency

Both dialogs now feature:
- Icon-based visual hierarchy with 48px icons
- Consistent button styling (gray cancel, blue confirm)
- Proper disabled states and hover effects
- Gray-50 background on action button areas
- Backdrop click to close

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-10-17 21:14:32 +02:00
parent ef16b9d060
commit 59e30cca8a
2 changed files with 227 additions and 95 deletions

View file

@ -1,14 +1,5 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Button,
FormControlLabel,
Checkbox,
} from '@mui/material';
import React, { useState, useEffect, useRef } from 'react';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { useTimelineStore } from '../../stores/timelineStore';
interface CreateStateDialogProps {
@ -23,9 +14,19 @@ const CreateStateDialog: React.FC<CreateStateDialogProps> = ({ open, onClose })
const [label, setLabel] = useState('');
const [description, setDescription] = useState('');
const [cloneFromCurrent, setCloneFromCurrent] = useState(true);
const inputRef = useRef<HTMLInputElement>(null);
const { createState } = useTimelineStore();
// Focus input when dialog opens
useEffect(() => {
if (open) {
setTimeout(() => {
inputRef.current?.focus();
}, 50);
}
}, [open]);
const handleCreate = () => {
if (!label.trim()) return;
@ -46,53 +47,129 @@ const CreateStateDialog: React.FC<CreateStateDialogProps> = ({ open, onClose })
onClose();
};
// Handle keyboard shortcuts
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleCreate();
} else if (e.key === 'Escape') {
e.preventDefault();
handleClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [open, label, description, cloneFromCurrent]); // eslint-disable-line react-hooks/exhaustive-deps
if (!open) return null;
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>Create New State</DialogTitle>
<DialogContent>
<div className="space-y-4 mt-2">
<TextField
autoFocus
fullWidth
label="State Label"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="e.g., 'January 2024' or 'Strategy A'"
helperText="Give this state a descriptive name"
/>
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={handleClose}
>
<div
className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"
onClick={(e) => e.stopPropagation()}
>
{/* Content */}
<div className="p-6">
<div className="flex items-start space-x-4">
{/* Icon */}
<div className="flex-shrink-0">
<AddCircleIcon className="text-blue-600" sx={{ fontSize: 48 }} />
</div>
<TextField
fullWidth
multiline
rows={3}
label="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional notes about this state..."
/>
{/* Text Content */}
<div className="flex-1 pt-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Create New State
</h3>
<p className="text-sm text-gray-600 mb-4">
Create a new timeline state to capture a different version of your graph
</p>
<FormControlLabel
control={
<Checkbox
checked={cloneFromCurrent}
onChange={(e) => setCloneFromCurrent(e.target.checked)}
/>
}
label="Clone current graph (uncheck for empty state)"
/>
{/* Form Fields */}
<div className="space-y-3">
{/* State Label */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State Label
</label>
<input
ref={inputRef}
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="e.g., 'January 2024' or 'Strategy A'"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
/>
<p className="mt-1 text-xs text-gray-500">
Give this state a descriptive name
</p>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description (optional)
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional notes about this state..."
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 resize-none"
/>
</div>
{/* Clone Checkbox */}
<div className="flex items-center">
<input
type="checkbox"
id="cloneFromCurrent"
checked={cloneFromCurrent}
onChange={(e) => setCloneFromCurrent(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor="cloneFromCurrent"
className="ml-2 block text-sm text-gray-700"
>
Clone current graph (uncheck for empty state)
</label>
</div>
</div>
</div>
</div>
</div>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button
onClick={handleCreate}
variant="contained"
disabled={!label.trim()}
>
Create State
</Button>
</DialogActions>
</Dialog>
{/* Actions */}
<div className="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end space-x-3">
<button
onClick={handleClose}
className="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
Cancel
</button>
<button
onClick={handleCreate}
disabled={!label.trim()}
className={`px-4 py-2 text-white text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 ${
!label.trim()
? 'bg-blue-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'
}`}
>
Create State
</button>
</div>
</div>
</div>
);
};

View file

@ -1,12 +1,5 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
} from '@mui/material';
import React, { useState, useEffect, useRef } from 'react';
import EditIcon from '@mui/icons-material/Edit';
interface RenameStateDialogProps {
open: boolean;
@ -25,12 +18,23 @@ const RenameStateDialog: React.FC<RenameStateDialogProps> = ({
onRename,
}) => {
const [label, setLabel] = useState(currentLabel);
const inputRef = useRef<HTMLInputElement>(null);
// Update label when currentLabel changes
useEffect(() => {
setLabel(currentLabel);
}, [currentLabel]);
// Focus input when dialog opens
useEffect(() => {
if (open) {
setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 50);
}
}, [open]);
const handleRename = () => {
if (label.trim()) {
onRename(label.trim());
@ -38,41 +42,92 @@ const RenameStateDialog: React.FC<RenameStateDialogProps> = ({
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleRename();
}
const handleCancel = () => {
setLabel(currentLabel); // Reset to original
onClose();
};
// Handle keyboard shortcuts
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleRename();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancel();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [open, label]); // eslint-disable-line react-hooks/exhaustive-deps
if (!open) return null;
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Rename State</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="State Label"
type="text"
fullWidth
variant="outlined"
value={label}
onChange={(e) => setLabel(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter state label"
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button
onClick={handleRename}
variant="contained"
disabled={!label.trim()}
>
Rename
</Button>
</DialogActions>
</Dialog>
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={handleCancel}
>
<div
className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"
onClick={(e) => e.stopPropagation()}
>
{/* Content */}
<div className="p-6">
<div className="flex items-start space-x-4">
{/* Icon */}
<div className="flex-shrink-0">
<EditIcon className="text-blue-600" sx={{ fontSize: 48 }} />
</div>
{/* Text Content */}
<div className="flex-1 pt-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Rename State
</h3>
<p className="text-sm text-gray-600 mb-3">
Enter a new name for this timeline state
</p>
{/* Input Field */}
<input
ref={inputRef}
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="Enter state label"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Actions */}
<div className="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end space-x-3">
<button
onClick={handleCancel}
className="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
Cancel
</button>
<button
onClick={handleRename}
disabled={!label.trim()}
className={`px-4 py-2 text-white text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 ${
!label.trim()
? 'bg-blue-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'
}`}
>
Rename
</button>
</div>
</div>
</div>
);
};