mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
feat: improve actor type form layout and UX
Consolidates name, color, and icon fields into a single horizontal row for more compact form layout. Removes progressive disclosure pattern to show all fields at once. Changes: - Single-row layout for name, color, and icon fields with proper alignment - Name field uses flex-1 with min-w-0 for proper text truncation - Color picker fixed at h-8 to match input heights - Icon popover uses fixed positioning (z-9999) to prevent clipping - All form fields now always visible (removed collapsible advanced options) - Removed edit panel header for cleaner UI - Removed border above action buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e0784ff3d8
commit
bc6ffb5bc3
3 changed files with 496 additions and 0 deletions
127
src/components/Config/EditTypeInline.tsx
Normal file
127
src/components/Config/EditTypeInline.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { useState, useEffect, useRef, KeyboardEvent } from 'react';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import TypeFormFields from './TypeFormFields';
|
||||||
|
import type { NodeTypeConfig, NodeShape } from '../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EditTypeInline - Inline edit view that replaces the right column
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Replaces management list in right column when editing
|
||||||
|
* - Reuses TypeFormFields
|
||||||
|
* - Save/Cancel actions
|
||||||
|
* - Keyboard accessible (Cmd/Ctrl+Enter to save, Escape to cancel)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: NodeTypeConfig;
|
||||||
|
onSave: (
|
||||||
|
id: string,
|
||||||
|
updates: {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
shape: NodeShape;
|
||||||
|
icon?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditTypeInline = ({ type, onSave, onCancel }: Props) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [color, setColor] = useState('#6366f1');
|
||||||
|
const [shape, setShape] = useState<NodeShape>('rectangle');
|
||||||
|
const [icon, setIcon] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
|
||||||
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Sync state with type prop
|
||||||
|
useEffect(() => {
|
||||||
|
if (type) {
|
||||||
|
setName(type.label);
|
||||||
|
setColor(type.color);
|
||||||
|
setShape(type.shape || 'rectangle');
|
||||||
|
setIcon(type.icon || '');
|
||||||
|
setDescription(type.description || '');
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
nameInputRef.current?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(type.id, {
|
||||||
|
label: name.trim(),
|
||||||
|
color,
|
||||||
|
shape,
|
||||||
|
icon: icon || undefined,
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-full">
|
||||||
|
{/* Form Fields */}
|
||||||
|
<div className="flex-1 mb-6">
|
||||||
|
<TypeFormFields
|
||||||
|
name={name}
|
||||||
|
color={color}
|
||||||
|
shape={shape}
|
||||||
|
icon={icon}
|
||||||
|
description={description}
|
||||||
|
onNameChange={setName}
|
||||||
|
onColorChange={setColor}
|
||||||
|
onShapeChange={setShape}
|
||||||
|
onIconChange={setIcon}
|
||||||
|
onDescriptionChange={setDescription}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
nameInputRef={nameInputRef}
|
||||||
|
autoFocusName={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="pt-6 space-y-3">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="flex-1 px-6 py-3 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="flex-1 px-6 py-3 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<SaveIcon fontSize="small" />
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Keyboard Shortcut Hint */}
|
||||||
|
<div className="text-xs text-gray-500 text-center">
|
||||||
|
<kbd className="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">
|
||||||
|
{navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl'}+Enter
|
||||||
|
</kbd>{' '}
|
||||||
|
to save, <kbd className="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">Esc</kbd> to cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditTypeInline;
|
||||||
248
src/components/Config/IconPopover.tsx
Normal file
248
src/components/Config/IconPopover.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
import { useState, useRef, useEffect, KeyboardEvent } from 'react';
|
||||||
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
|
import GroupIcon from '@mui/icons-material/Group';
|
||||||
|
import BusinessIcon from '@mui/icons-material/Business';
|
||||||
|
import ComputerIcon from '@mui/icons-material/Computer';
|
||||||
|
import CloudIcon from '@mui/icons-material/Cloud';
|
||||||
|
import StorageIcon from '@mui/icons-material/Storage';
|
||||||
|
import DevicesIcon from '@mui/icons-material/Devices';
|
||||||
|
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||||
|
import CategoryIcon from '@mui/icons-material/Category';
|
||||||
|
import LightbulbIcon from '@mui/icons-material/Lightbulb';
|
||||||
|
import WorkIcon from '@mui/icons-material/Work';
|
||||||
|
import SchoolIcon from '@mui/icons-material/School';
|
||||||
|
import LocalHospitalIcon from '@mui/icons-material/LocalHospital';
|
||||||
|
import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
|
||||||
|
import StoreIcon from '@mui/icons-material/Store';
|
||||||
|
import FactoryIcon from '@mui/icons-material/Factory';
|
||||||
|
import EngineeringIcon from '@mui/icons-material/Engineering';
|
||||||
|
import ScienceIcon from '@mui/icons-material/Science';
|
||||||
|
import PublicIcon from '@mui/icons-material/Public';
|
||||||
|
import LocationCityIcon from '@mui/icons-material/LocationCity';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IconPopover - Floating popover icon picker
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Button trigger with current icon preview
|
||||||
|
* - Floating popover (no nested scrolling)
|
||||||
|
* - Grid display of available icons
|
||||||
|
* - Visual selection feedback
|
||||||
|
* - Keyboard accessible (Escape to close, Arrow keys to navigate)
|
||||||
|
* - Click outside to close
|
||||||
|
* - ARIA compliant
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedIcon?: string;
|
||||||
|
onSelect: (iconName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available icons with their names
|
||||||
|
const availableIcons = [
|
||||||
|
{ name: 'Person', component: PersonIcon },
|
||||||
|
{ name: 'Group', component: GroupIcon },
|
||||||
|
{ name: 'Business', component: BusinessIcon },
|
||||||
|
{ name: 'Computer', component: ComputerIcon },
|
||||||
|
{ name: 'Cloud', component: CloudIcon },
|
||||||
|
{ name: 'Storage', component: StorageIcon },
|
||||||
|
{ name: 'Devices', component: DevicesIcon },
|
||||||
|
{ name: 'AccountTree', component: AccountTreeIcon },
|
||||||
|
{ name: 'Category', component: CategoryIcon },
|
||||||
|
{ name: 'Lightbulb', component: LightbulbIcon },
|
||||||
|
{ name: 'Work', component: WorkIcon },
|
||||||
|
{ name: 'School', component: SchoolIcon },
|
||||||
|
{ name: 'LocalHospital', component: LocalHospitalIcon },
|
||||||
|
{ name: 'AccountBalance', component: AccountBalanceIcon },
|
||||||
|
{ name: 'Store', component: StoreIcon },
|
||||||
|
{ name: 'Factory', component: FactoryIcon },
|
||||||
|
{ name: 'Engineering', component: EngineeringIcon },
|
||||||
|
{ name: 'Science', component: ScienceIcon },
|
||||||
|
{ name: 'Public', component: PublicIcon },
|
||||||
|
{ name: 'LocationCity', component: LocationCityIcon },
|
||||||
|
];
|
||||||
|
|
||||||
|
const IconPopover = ({ selectedIcon, onSelect }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Find the selected icon component
|
||||||
|
const selectedIconObj = availableIcons.find((icon) => icon.name === selectedIcon);
|
||||||
|
const SelectedIconComponent = selectedIconObj?.component;
|
||||||
|
|
||||||
|
// Close on click outside
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
popoverRef.current &&
|
||||||
|
!popoverRef.current.contains(event.target as Node) &&
|
||||||
|
buttonRef.current &&
|
||||||
|
!buttonRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Close on Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const handleEscape = (event: globalThis.KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleSelectIcon = (iconName: string) => {
|
||||||
|
onSelect(iconName);
|
||||||
|
setIsOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent, iconName: string, index: number) => {
|
||||||
|
const gridCols = 8;
|
||||||
|
const totalItems = availableIcons.length + 1; // +1 for "no icon" option
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectIcon(iconName);
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusedIndex(Math.min(index + 1, totalItems - 1));
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusedIndex(Math.max(index - 1, 0));
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusedIndex(Math.min(index + gridCols, totalItems - 1));
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusedIndex(Math.max(index - gridCols, 0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-focus when navigating with keyboard
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusedIndex >= 0 && popoverRef.current) {
|
||||||
|
const buttons = popoverRef.current.querySelectorAll('button');
|
||||||
|
const button = buttons[focusedIndex] as HTMLButtonElement;
|
||||||
|
button?.focus();
|
||||||
|
}
|
||||||
|
}, [focusedIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* Trigger Button */}
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-label={selectedIcon ? `Selected icon: ${selectedIcon}. Click to change` : 'No icon selected. Click to choose'}
|
||||||
|
>
|
||||||
|
{SelectedIconComponent ? (
|
||||||
|
<>
|
||||||
|
<SelectedIconComponent fontSize="small" className="text-gray-700" />
|
||||||
|
<span className="text-sm text-gray-700">{selectedIcon}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-500">No icon</span>
|
||||||
|
)}
|
||||||
|
<CloseIcon
|
||||||
|
fontSize="small"
|
||||||
|
className="text-gray-400 ml-auto"
|
||||||
|
style={{ transform: isOpen ? 'rotate(45deg)' : 'none', transition: 'transform 0.2s' }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Popover */}
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className="fixed bg-white border border-gray-300 rounded-lg shadow-lg z-[9999] p-3"
|
||||||
|
style={{
|
||||||
|
width: '320px',
|
||||||
|
top: buttonRef.current ? `${buttonRef.current.getBoundingClientRect().bottom + 4}px` : '0',
|
||||||
|
left: buttonRef.current ? `${buttonRef.current.getBoundingClientRect().left}px` : '0',
|
||||||
|
}}
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Icon picker"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-8 gap-2 max-h-64 overflow-y-auto">
|
||||||
|
{/* No icon option */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectIcon('')}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, '', 0)}
|
||||||
|
className={`
|
||||||
|
p-2 rounded border-2 transition-all flex items-center justify-center
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1
|
||||||
|
${!selectedIcon
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title="No icon"
|
||||||
|
aria-label="No icon"
|
||||||
|
aria-pressed={!selectedIcon}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-gray-500">—</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Icon options */}
|
||||||
|
{availableIcons.map((icon, index) => {
|
||||||
|
const IconComponent = icon.component;
|
||||||
|
const actualIndex = index + 1; // +1 because "no icon" is at index 0
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={icon.name}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectIcon(icon.name)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, icon.name, actualIndex)}
|
||||||
|
className={`
|
||||||
|
p-2 rounded border-2 transition-all flex items-center justify-center
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1
|
||||||
|
${selectedIcon === icon.name
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={icon.name}
|
||||||
|
aria-label={icon.name}
|
||||||
|
aria-pressed={selectedIcon === icon.name}
|
||||||
|
>
|
||||||
|
<IconComponent fontSize="small" className="text-gray-700" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconPopover;
|
||||||
121
src/components/Config/TypeFormFields.tsx
Normal file
121
src/components/Config/TypeFormFields.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { KeyboardEvent } from 'react';
|
||||||
|
import IconPopover from './IconPopover';
|
||||||
|
import ShapeSelector from './ShapeSelector';
|
||||||
|
import type { NodeShape } from '../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeFormFields - Reusable form fields for add/edit actor types
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - All fields visible
|
||||||
|
* - Keyboard accessible
|
||||||
|
* - Consistent between add and edit modes
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
shape: NodeShape;
|
||||||
|
icon: string;
|
||||||
|
description: string;
|
||||||
|
onNameChange: (value: string) => void;
|
||||||
|
onColorChange: (value: string) => void;
|
||||||
|
onShapeChange: (value: NodeShape) => void;
|
||||||
|
onIconChange: (value: string) => void;
|
||||||
|
onDescriptionChange: (value: string) => void;
|
||||||
|
onKeyDown?: (e: KeyboardEvent) => void;
|
||||||
|
nameInputRef?: React.RefObject<HTMLInputElement>;
|
||||||
|
autoFocusName?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypeFormFields = ({
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
shape,
|
||||||
|
icon,
|
||||||
|
description,
|
||||||
|
onNameChange,
|
||||||
|
onColorChange,
|
||||||
|
onShapeChange,
|
||||||
|
onIconChange,
|
||||||
|
onDescriptionChange,
|
||||||
|
onKeyDown,
|
||||||
|
nameInputRef,
|
||||||
|
autoFocusName = false,
|
||||||
|
}: Props) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Name, Color, and Icon - Single row */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
{/* Name */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<label htmlFor="type-name" className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Name <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="type-name"
|
||||||
|
ref={nameInputRef}
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder="e.g., Department"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-required="true"
|
||||||
|
autoFocus={autoFocusName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color */}
|
||||||
|
<div className="w-20 flex-shrink-0">
|
||||||
|
<label htmlFor="type-color-picker" className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Color <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="type-color-picker"
|
||||||
|
type="color"
|
||||||
|
value={color}
|
||||||
|
onChange={(e) => onColorChange(e.target.value)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
className="h-8 w-full rounded cursor-pointer border border-gray-300"
|
||||||
|
aria-label="Color picker"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Icon
|
||||||
|
</label>
|
||||||
|
<IconPopover selectedIcon={icon} onSelect={onIconChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shape Selector */}
|
||||||
|
<ShapeSelector value={shape} onChange={onShapeChange} color={color} />
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="type-description"
|
||||||
|
className="block text-xs font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Description (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="type-description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||||
|
placeholder="Brief description of this actor type"
|
||||||
|
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-blue-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeFormFields;
|
||||||
Loading…
Reference in a new issue