Files
apparel-designer/client/src/components/panels/PropertiesPanel.jsx
Khalid A 009557c249 Implement template system and PWA enhancements
Phase 6 - Template System:
- Add TemplateLayer component for background/overlay rendering
- Add SlotPlaceholder component with visual indicators for empty slots
- Add useTemplate hook with auto-crop and drag constraint functions
- Update templates.js with slot definitions for team-sport template
- Integrate template system into DesignCanvas and App
- Add slot upload UI in TemplatesTab sidebar

Phase 9 - PWA Improvements:
- Add Workbox caching rules for HuggingFace LFS, templates, and API
- Change registerType to 'prompt' for update notifications
- Add service worker update handler in main.jsx
- Add refresh prompt UI in PWAInstall component

Phase 10 - Responsive and Accessibility:
- Add responsive CSS media queries for tablet/mobile layouts
- Add OfflineIndicator component with online/offline detection
- Add focus trap and keyboard navigation to PhotoPreEditor
- Add aria labels and screen reader support to modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 22:08:22 -05:00

315 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { memo } from 'react';
export const PropertiesPanel = memo(function PropertiesPanel({ element, onUpdate, onDelete, onEditPhoto }) {
if (!element) {
return (
<div className="properties-panel">
<div style={{
padding: '1rem',
borderBottom: `1px solid var(--border)`,
}}>
<h3 style={{
margin: 0,
fontSize: '14px',
fontWeight: '600',
color: 'var(--text-primary)',
}}>
Properties
</h3>
</div>
<div style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
color: 'var(--text-muted)',
fontSize: '12px',
textAlign: 'center',
}}>
Select an element to edit its properties
</div>
</div>
);
}
const handlePositionChange = (axis, value) => {
onUpdate({ [axis]: parseFloat(value) || 0 });
};
const handleSizeChange = (axis, value) => {
const numValue = parseFloat(value) || 20;
onUpdate({ [axis]: Math.max(20, numValue) });
};
const handleRotationChange = (value) => {
const numValue = parseFloat(value) || 0;
onUpdate({ rotation: Math.max(-180, Math.min(180, numValue)) });
};
return (
<div className="properties-panel">
<div style={{
padding: '1rem',
borderBottom: `1px solid var(--border)`,
}}>
<h3 style={{
margin: 0,
fontSize: '14px',
fontWeight: '600',
color: 'var(--text-primary)',
}}>
Properties
</h3>
</div>
<div style={{
flex: 1,
overflow: 'auto',
padding: '1rem',
}}>
{/* Element type badge */}
<div style={{
display: 'inline-block',
padding: '4px 8px',
background: 'var(--accent-bg)',
borderRadius: 'var(--radius-sm)',
fontSize: '11px',
fontWeight: '600',
color: 'var(--accent)',
textTransform: 'uppercase',
marginBottom: '1rem',
}}>
{element.type}
</div>
{/* Position */}
<div style={{ marginBottom: '1rem' }}>
<label style={{
display: 'block',
fontSize: '11px',
fontWeight: '600',
color: 'var(--text-secondary)',
marginBottom: '0.5rem',
textTransform: 'uppercase',
}}>
Position
</label>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<div style={{ flex: 1 }}>
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>X</label>
<input
type="number"
value={Math.round(element.x)}
onChange={(e) => handlePositionChange('x', e.target.value)}
style={{
width: '100%',
padding: '0.5rem',
border: `1px solid var(--border)`,
borderRadius: 'var(--radius-sm)',
fontSize: '13px',
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Y</label>
<input
type="number"
value={Math.round(element.y)}
onChange={(e) => handlePositionChange('y', e.target.value)}
style={{
width: '100%',
padding: '0.5rem',
border: `1px solid var(--border)`,
borderRadius: 'var(--radius-sm)',
fontSize: '13px',
}}
/>
</div>
</div>
</div>
{/* Size (for images and stickers) */}
{(element.type === 'image' || element.type === 'sticker') && (
<div style={{ marginBottom: '1rem' }}>
<label style={{
display: 'block',
fontSize: '11px',
fontWeight: '600',
color: 'var(--text-secondary)',
marginBottom: '0.5rem',
textTransform: 'uppercase',
}}>
Size
</label>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<div style={{ flex: 1 }}>
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>W</label>
<input
type="number"
value={Math.round(element.width)}
onChange={(e) => handleSizeChange('width', e.target.value)}
style={{
width: '100%',
padding: '0.5rem',
border: `1px solid var(--border)`,
borderRadius: 'var(--radius-sm)',
fontSize: '13px',
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>H</label>
<input
type="number"
value={Math.round(element.height)}
onChange={(e) => handleSizeChange('height', e.target.value)}
style={{
width: '100%',
padding: '0.5rem',
border: `1px solid var(--border)`,
borderRadius: 'var(--radius-sm)',
fontSize: '13px',
}}
/>
</div>
</div>
</div>
)}
{/* Edit Photo button (for images only) */}
{element.type === 'image' && onEditPhoto && (
<div style={{ marginBottom: '1rem' }}>
<button
onClick={() => onEditPhoto(element)}
style={{
width: '100%',
padding: '0.75rem',
border: `1px solid var(--accent)`,
borderRadius: 'var(--radius-md)',
background: 'var(--accent-bg)',
color: 'var(--accent)',
fontSize: '13px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
e.target.style.background = 'var(--accent)';
e.target.style.color = '#fff';
}}
onMouseLeave={(e) => {
e.target.style.background = 'var(--accent-bg)';
e.target.style.color = 'var(--accent)';
}}
>
Edit Photo
</button>
</div>
)}
{/* Font size (for text) */}
{element.type === 'text' && (
<>
<div style={{ marginBottom: '1rem' }}>
<label style={{
display: 'block',
fontSize: '11px',
fontWeight: '600',
color: 'var(--text-secondary)',
marginBottom: '0.5rem',
textTransform: 'uppercase',
}}>
Font Size: {Math.round(element.fontSize)}px
</label>
<input
type="range"
min="12"
max="120"
value={element.fontSize}
onChange={(e) => onUpdate({ fontSize: parseInt(e.target.value, 10) })}
style={{ width: '100%' }}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={{
display: 'block',
fontSize: '11px',
fontWeight: '600',
color: 'var(--text-secondary)',
marginBottom: '0.5rem',
textTransform: 'uppercase',
}}>
Color
</label>
<input
type="color"
value={element.fill}
onChange={(e) => onUpdate({ fill: e.target.value })}
style={{
width: '100%',
height: '36px',
border: `1px solid var(--border)`,
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
padding: '2px',
}}
/>
</div>
</>
)}
{/* Rotation */}
<div style={{ marginBottom: '1rem' }}>
<label style={{
display: 'block',
fontSize: '11px',
fontWeight: '600',
color: 'var(--text-secondary)',
marginBottom: '0.5rem',
textTransform: 'uppercase',
}}>
Rotation: {Math.round(element.rotation)}°
</label>
<input
type="range"
min="-180"
max="180"
value={element.rotation}
onChange={(e) => handleRotationChange(e.target.value)}
style={{ width: '100%' }}
/>
</div>
{/* Delete button */}
<button
onClick={() => onDelete(element.id)}
style={{
width: '100%',
padding: '0.75rem',
border: 'none',
borderRadius: 'var(--radius-md)',
background: 'var(--error)',
color: '#fff',
fontSize: '13px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.15s ease',
marginTop: '1rem',
}}
onMouseEnter={(e) => {
e.target.style.background = '#dc2626';
}}
onMouseLeave={(e) => {
e.target.style.background = 'var(--error)';
}}
>
Delete Element
</button>
</div>
</div>
);
});