| import React from 'react'; |
| import { CloseIcon, FullscreenEnterIcon, FullscreenExitIcon } from './icons'; |
| import { AiImageQuality } from '../hooks/useAiFeatures'; |
|
|
| interface SettingsPanelProps { |
| isOpen: boolean; |
| onClose: () => void; |
| showZoomSlider: boolean; |
| onShowZoomSliderChange: (show: boolean) => void; |
| aiImageQuality: AiImageQuality; |
| onAiImageQualityChange: (quality: AiImageQuality) => void; |
| aiApiEndpoint: string; |
| onAiApiEndpointChange: (endpoint: string) => void; |
| isFullscreenActive: boolean; |
| onToggleFullscreen: () => void; |
| currentCanvasWidth: number; |
| currentCanvasHeight: number; |
| onCanvasSizeChange: (width: number, height: number) => void; |
| } |
|
|
| const CANVAS_SIZE_OPTIONS = [ |
| { label: '1000 x 1000 px', width: 1000, height: 1000 }, |
| { label: '1200 x 1200 px', width: 1200, height: 1200 }, |
| { label: '1600 x 1600 px', width: 1600, height: 1600 }, |
| { label: '2000 x 2000 px', width: 2000, height: 2000 }, |
| { label: '4000 x 4000 px', width: 4000, height: 4000 }, |
| ]; |
|
|
| const SettingsPanel: React.FC<SettingsPanelProps> = ({ |
| isOpen, |
| onClose, |
| showZoomSlider, |
| onShowZoomSliderChange, |
| aiImageQuality, |
| onAiImageQualityChange, |
| aiApiEndpoint, |
| onAiApiEndpointChange, |
| isFullscreenActive, |
| onToggleFullscreen, |
| currentCanvasWidth, |
| currentCanvasHeight, |
| onCanvasSizeChange, |
| }) => { |
| if (!isOpen) return null; |
|
|
| const qualityOptions: { label: string; value: AiImageQuality }[] = [ |
| { label: 'Low', value: 'low' }, |
| { label: 'Medium', value: 'medium' }, |
| { label: 'High (HD)', value: 'hd' }, |
| ]; |
|
|
| const handleSizeSelection = (event: React.ChangeEvent<HTMLSelectElement>) => { |
| const selectedValue = event.target.value; |
| const [newWidth, newHeight] = selectedValue.split('x').map(Number); |
| if (!isNaN(newWidth) && !isNaN(newHeight)) { |
| onCanvasSizeChange(newWidth, newHeight); |
| } |
| }; |
|
|
| const currentSizeValue = `${currentCanvasWidth}x${currentCanvasHeight}`; |
|
|
| return ( |
| <div |
| className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex justify-center items-center z-[1000] p-4" |
| aria-modal="true" |
| role="dialog" |
| aria-labelledby="settings-panel-title" |
| > |
| <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-md transform transition-all overflow-y-auto max-h-[90vh]"> |
| <div className="flex justify-between items-center mb-6"> |
| <h2 id="settings-panel-title" className="text-xl font-semibold text-slate-800"> |
| Application Settings |
| </h2> |
| <button |
| onClick={onClose} |
| className="text-slate-400 hover:text-slate-600 transition-colors" |
| aria-label="Close settings panel" |
| > |
| <CloseIcon className="w-6 h-6" /> |
| </button> |
| </div> |
| |
| {/* Interface Settings */} |
| <div className="mb-6"> |
| <h3 className="text-md font-medium text-slate-700 mb-2">Interface</h3> |
| <div className="space-y-3"> |
| {/* Zoom Slider Toggle */} |
| <div className="flex items-center justify-between bg-slate-50 p-3 rounded-md border border-slate-200"> |
| <label htmlFor="showZoomSliderToggle" className="text-sm text-slate-600"> |
| Show Zoom Slider |
| </label> |
| <button |
| id="showZoomSliderToggle" |
| role="switch" |
| aria-checked={showZoomSlider} |
| onClick={() => onShowZoomSliderChange(!showZoomSlider)} |
| className={`relative inline-flex items-center h-6 rounded-full w-11 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${ |
| showZoomSlider ? 'bg-blue-600' : 'bg-slate-300' |
| }`} |
| > |
| <span className="sr-only">Toggle Zoom Slider Visibility</span> |
| <span |
| className={`inline-block w-4 h-4 transform bg-white rounded-full transition-transform duration-200 ease-in-out ${ |
| showZoomSlider ? 'translate-x-6' : 'translate-x-1' |
| }`} |
| /> |
| </button> |
| </div> |
| |
| {/* Fullscreen Toggle */} |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200"> |
| <button |
| onClick={onToggleFullscreen} |
| className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors" |
| aria-label={isFullscreenActive ? 'Exit Fullscreen' : 'Enter Fullscreen'} |
| > |
| {isFullscreenActive ? ( |
| <FullscreenExitIcon className="w-5 h-5 mr-2" /> |
| ) : ( |
| <FullscreenEnterIcon className="w-5 h-5 mr-2" /> |
| )} |
| {isFullscreenActive ? 'Exit Fullscreen' : 'Enter Fullscreen'} |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {/* Canvas Dimensions Settings */} |
| <div className="mb-6"> |
| <h3 className="text-md font-medium text-slate-700 mb-2">Canvas Dimensions</h3> |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200"> |
| <label htmlFor="canvasSizeSelect" className="block text-sm text-slate-600 mb-1"> |
| Canvas Size |
| </label> |
| <select |
| id="canvasSizeSelect" |
| value={currentSizeValue} |
| onChange={handleSizeSelection} |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" |
| aria-describedby="canvas-size-description" |
| > |
| {CANVAS_SIZE_OPTIONS.map((option) => ( |
| <option key={`${option.width}x${option.height}`} value={`${option.width}x${option.height}`}> |
| {option.label} |
| </option> |
| ))} |
| </select> |
| <p id="canvas-size-description" className="text-xs text-slate-500 mt-1"> |
| Changing size will clear the current canvas and history. |
| </p> |
| </div> |
| </div> |
| |
| |
| {/* AI Image Generation Settings */} |
| <div className="mb-6"> |
| <h3 className="text-md font-medium text-slate-700 mb-2">AI Image Generation</h3> |
| |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4"> |
| <label htmlFor="aiApiEndpointInput" className="block text-sm text-slate-600 mb-1"> |
| API Endpoint URL |
| </label> |
| <input |
| type="text" |
| id="aiApiEndpointInput" |
| value={aiApiEndpoint} |
| onChange={(e) => onAiApiEndpointChange(e.target.value)} |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" |
| placeholder="Enter AI API URL" |
| aria-describedby="ai-api-description" |
| /> |
| <p id="ai-api-description" className="text-xs text-slate-500 mt-1"> |
| Use <code className="bg-slate-200 px-1 rounded">{'{prompt}'}</code> for text prompt and <code className="bg-slate-200 px-1 rounded">{'{imgurl.url}'}</code> for image URL. |
| </p> |
| </div> |
| |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200"> |
| <label htmlFor="aiImageQualitySelect" className="block text-sm text-slate-600 mb-1"> |
| Image Quality (Pollinations API) |
| </label> |
| <select |
| id="aiImageQualitySelect" |
| value={aiImageQuality} |
| onChange={(e) => onAiImageQualityChange(e.target.value as AiImageQuality)} |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" |
| aria-describedby="ai-quality-description" |
| > |
| {qualityOptions.map((option) => ( |
| <option key={option.value} value={option.value}> |
| {option.label} |
| </option> |
| ))} |
| </select> |
| <p id="ai-quality-description" className="text-xs text-slate-500 mt-1"> |
| This quality setting primarily applies to Pollinations-like APIs. Custom endpoints must manage their own quality parameters if needed. |
| </p> |
| </div> |
| </div> |
| |
| <div className="flex justify-end gap-3 mt-8"> |
| <button |
| onClick={onClose} |
| className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-sm font-medium" |
| > |
| Done |
| </button> |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default SettingsPanel; |