diff --git a/public/themes-manifest.json b/public/themes-manifest.json index f15d1ad..b6c19f3 100644 --- a/public/themes-manifest.json +++ b/public/themes-manifest.json @@ -12,5 +12,5 @@ "hasMasterSlide": true } }, - "generated": "2025-08-21T16:18:27.749Z" + "generated": "2025-08-21T16:40:45.572Z" } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7e04051..639809d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { NewPresentationPage } from './components/presentations/NewPresentationP import { PresentationViewer } from './components/presentations/PresentationViewer.tsx'; import { PresentationMode } from './components/presentations/PresentationMode.tsx'; import { PresentationEditor } from './components/presentations/PresentationEditor.tsx'; -import { SlideEditor } from './components/slide-editor/index.ts'; +import { SlideEditor } from './components/slide-editor/SlideEditor.tsx'; import { PresentationsList } from './components/presentations/PresentationsList.tsx'; import { AppHeader } from './components/AppHeader.tsx'; import { Welcome } from './components/Welcome.tsx'; diff --git a/src/components/presentations/NewPresentationPage.tsx b/src/components/presentations/NewPresentationPage.tsx index 9caf2c3..275c82f 100644 --- a/src/components/presentations/NewPresentationPage.tsx +++ b/src/components/presentations/NewPresentationPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import type { Theme } from '../../types/theme.ts'; import type { AspectRatio } from '../../types/presentation.ts'; -import { getThemes } from '../../themes/index.ts'; +import { discoverThemes as getThemes } from '../../utils/themeLoader.ts'; import { createPresentation } from '../../utils/presentationStorage.ts'; import { loggers } from '../../utils/logger.ts'; import { AlertDialog } from '../ui/AlertDialog.tsx'; diff --git a/src/components/presentations/PresentationEditor.tsx b/src/components/presentations/PresentationEditor.tsx index aeaa1bd..68cfaa7 100644 --- a/src/components/presentations/PresentationEditor.tsx +++ b/src/components/presentations/PresentationEditor.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate, Link } from 'react-router-dom'; import type { Presentation } from '../../types/presentation.ts'; import type { Theme } from '../../types/theme.ts'; import { getPresentationById, updatePresentation } from '../../utils/presentationStorage.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import { useDialog } from '../../hooks/useDialog.ts'; import { AlertDialog } from '../ui/AlertDialog.tsx'; import { ConfirmDialog } from '../ui/ConfirmDialog.tsx'; @@ -69,7 +69,7 @@ export const PresentationEditor: React.FC = () => { setPresentation(presentationData); // Load theme - const themeData = await getTheme(presentationData.metadata.theme); + const themeData = await loadTheme(presentationData.metadata.theme, false); if (!themeData) { setError(`Theme not found: ${presentationData.metadata.theme}`); return; diff --git a/src/components/presentations/PresentationMode.tsx b/src/components/presentations/PresentationMode.tsx index 0fcaf2b..13de5fb 100644 --- a/src/components/presentations/PresentationMode.tsx +++ b/src/components/presentations/PresentationMode.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import type { Presentation, SlideContent } from '../../types/presentation.ts'; import type { Theme } from '../../types/theme.ts'; import { getPresentationById } from '../../utils/presentationStorage.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; import { loggers } from '../../utils/logger.ts'; @@ -116,7 +116,7 @@ export const PresentationMode: React.FC = () => { setPresentation(presentationData); // Load theme - const themeData = await getTheme(presentationData.metadata.theme); + const themeData = await loadTheme(presentationData.metadata.theme, false); if (!themeData) { setError(`Theme not found: ${presentationData.metadata.theme}`); return; diff --git a/src/components/presentations/PresentationViewer.tsx b/src/components/presentations/PresentationViewer.tsx index 49d335d..7775c46 100644 --- a/src/components/presentations/PresentationViewer.tsx +++ b/src/components/presentations/PresentationViewer.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate, Link } from 'react-router-dom'; import type { Presentation } from '../../types/presentation.ts'; import type { Theme } from '../../types/theme.ts'; import { getPresentationById } from '../../utils/presentationStorage.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import './PresentationViewer.css'; export const PresentationViewer: React.FC = () => { @@ -41,7 +41,7 @@ export const PresentationViewer: React.FC = () => { setPresentation(presentationData); // Load theme - const themeData = await getTheme(presentationData.metadata.theme); + const themeData = await loadTheme(presentationData.metadata.theme, false); if (!themeData) { setError(`Theme not found: ${presentationData.metadata.theme}`); return; diff --git a/src/components/presentations/index.ts b/src/components/presentations/index.ts deleted file mode 100644 index 6bc23c2..0000000 --- a/src/components/presentations/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { NewPresentationPage } from './NewPresentationPage.tsx'; -export { PresentationViewer } from './PresentationViewer.tsx'; -export { PresentationEditor } from './PresentationEditor.tsx'; -export { ThemeSelector } from './ThemeSelector.tsx'; -export { PresentationsList } from './PresentationsList.tsx'; \ No newline at end of file diff --git a/src/components/slide-editor/ContentEditor.tsx b/src/components/slide-editor/ContentEditor.tsx index 03efe0e..416b4b3 100644 --- a/src/components/slide-editor/ContentEditor.tsx +++ b/src/components/slide-editor/ContentEditor.tsx @@ -4,6 +4,8 @@ import type { SlideLayout } from '../../types/theme.ts'; import { renderTemplateWithContent } from './utils.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; import { ImageUploadField } from '../ui/ImageUploadField.tsx'; +import { CancelLink } from '../ui/buttons/CancelLink.tsx'; +import { ActionButton } from '../ui/buttons/ActionButton.tsx'; interface ContentEditorProps { presentation: Presentation; @@ -109,24 +111,22 @@ export const ContentEditor: React.FC = ({
- +
- + {isEditingExisting ? 'Update Slide' : 'Save Slide'} +
diff --git a/src/components/slide-editor/SlideEditor.tsx b/src/components/slide-editor/SlideEditor.tsx index b30300e..630601c 100644 --- a/src/components/slide-editor/SlideEditor.tsx +++ b/src/components/slide-editor/SlideEditor.tsx @@ -6,6 +6,7 @@ import { ErrorState } from './ErrorState.tsx'; import { LayoutSelection } from './LayoutSelection.tsx'; import { ContentEditor } from './ContentEditor.tsx'; import { SlidePreviewModal } from './SlidePreviewModal.tsx'; +import { ActionButton } from '../ui/buttons/ActionButton.tsx'; import { useSlideEditor } from './useSlideEditor.ts'; import './SlideEditor.css'; @@ -75,13 +76,12 @@ export const SlideEditor: React.FC = () => {
{currentStep === 'content' && selectedLayout && ( - + )}
diff --git a/src/components/slide-editor/SlidePreviewModal.tsx b/src/components/slide-editor/SlidePreviewModal.tsx index c5b8d12..f865a0b 100644 --- a/src/components/slide-editor/SlidePreviewModal.tsx +++ b/src/components/slide-editor/SlidePreviewModal.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import type { SlideLayout } from '../../types/theme.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; +import { CloseButton } from '../ui/buttons/CloseButton.tsx'; interface SlidePreviewModalProps { isOpen: boolean; @@ -87,14 +88,12 @@ export const SlidePreviewModal: React.FC = ({ )} {/* Close button */} - + /> {/* Theme info */}
diff --git a/src/components/slide-editor/index.ts b/src/components/slide-editor/index.ts deleted file mode 100644 index dcb9899..0000000 --- a/src/components/slide-editor/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { SlideEditor } from './SlideEditor.tsx'; -export { SlidePreviewModal } from './SlidePreviewModal.tsx'; -export { LayoutSelection } from './LayoutSelection.tsx'; -export { ContentEditor } from './ContentEditor.tsx'; -export { useSlideEditor } from './useSlideEditor.ts'; -export { SlideEditorErrorBoundary } from './SlideEditorErrorBoundary.tsx'; \ No newline at end of file diff --git a/src/components/slide-editor/useSlideEditor.ts b/src/components/slide-editor/useSlideEditor.ts index 39b4e76..46dc911 100644 --- a/src/components/slide-editor/useSlideEditor.ts +++ b/src/components/slide-editor/useSlideEditor.ts @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import type { Presentation, SlideContent } from '../../types/presentation.ts'; import type { Theme, SlideLayout } from '../../types/theme.ts'; import { getPresentationById, updatePresentation } from '../../utils/presentationStorage.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import { loggers } from '../../utils/logger.ts'; interface UseSlideEditorProps { @@ -91,7 +91,7 @@ export const useSlideEditor = ({ presentationId, slideId }: UseSlideEditorProps) setPresentation(presentationData); // Load theme - const themeData = await getTheme(presentationData.metadata.theme); + const themeData = await loadTheme(presentationData.metadata.theme, false); if (!themeData) { setError(`Theme not found: ${presentationData.metadata.theme}`); return; diff --git a/src/components/themes/LayoutDetailPage.tsx b/src/components/themes/LayoutDetailPage.tsx index fd51123..0136451 100644 --- a/src/components/themes/LayoutDetailPage.tsx +++ b/src/components/themes/LayoutDetailPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import type { Theme, SlideLayout } from '../../types/theme.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import './LayoutDetailPage.css'; export const LayoutDetailPage: React.FC = () => { @@ -22,7 +22,7 @@ export const LayoutDetailPage: React.FC = () => { try { setLoading(true); - const themeData = await getTheme(themeId); + const themeData = await loadTheme(themeId, false); if (!themeData) { setError(`Theme "${themeId}" not found`); return; diff --git a/src/components/themes/LayoutPreviewPage.tsx b/src/components/themes/LayoutPreviewPage.tsx index 33f3043..4bfb6eb 100644 --- a/src/components/themes/LayoutPreviewPage.tsx +++ b/src/components/themes/LayoutPreviewPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import type { Theme, SlideLayout } from '../../types/theme.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import { renderTemplateWithSampleData } from '../../utils/templateRenderer.ts'; import { sanitizeSlideTemplate } from '../../utils/htmlSanitizer.ts'; import './LayoutPreviewPage.css'; @@ -24,7 +24,7 @@ export const LayoutPreviewPage: React.FC = () => { try { setLoading(true); - const themeData = await getTheme(themeId); + const themeData = await loadTheme(themeId, false); if (!themeData) { setError(`Theme "${themeId}" not found`); return; diff --git a/src/components/themes/ThemeBrowser.tsx b/src/components/themes/ThemeBrowser.tsx index 03bb419..45e4609 100644 --- a/src/components/themes/ThemeBrowser.tsx +++ b/src/components/themes/ThemeBrowser.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import type { Theme } from '../../types/theme.ts'; -import { getThemes } from '../../themes/index.ts'; +import { discoverThemes as getThemes } from '../../utils/themeLoader.ts'; import { LayoutPreview } from './LayoutPreview.tsx'; export const ThemeBrowser: React.FC = () => { diff --git a/src/components/themes/ThemeDetailPage.tsx b/src/components/themes/ThemeDetailPage.tsx index 8407a73..2077ea2 100644 --- a/src/components/themes/ThemeDetailPage.tsx +++ b/src/components/themes/ThemeDetailPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import type { Theme } from '../../types/theme.ts'; -import { getTheme } from '../../themes/index.ts'; +import { loadTheme } from '../../utils/themeLoader.ts'; import './ThemeDetailPage.css'; export const ThemeDetailPage: React.FC = () => { @@ -11,7 +11,7 @@ export const ThemeDetailPage: React.FC = () => { const [error, setError] = useState(null); useEffect(() => { - const loadTheme = async () => { + const loadThemeData = async () => { if (!themeId) { setError('No theme ID provided'); setLoading(false); @@ -20,7 +20,7 @@ export const ThemeDetailPage: React.FC = () => { try { setLoading(true); - const themeData = await getTheme(themeId); + const themeData = await loadTheme(themeId, false); if (!themeData) { setError(`Theme "${themeId}" not found`); return; @@ -33,7 +33,7 @@ export const ThemeDetailPage: React.FC = () => { } }; - loadTheme(); + loadThemeData(); }, [themeId]); if (loading) { diff --git a/src/components/themes/index.ts b/src/components/themes/index.ts deleted file mode 100644 index 4e7622b..0000000 --- a/src/components/themes/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { ThemeBrowser } from './ThemeBrowser.tsx'; -export { LayoutPreview } from './LayoutPreview.tsx'; -export { ThemeDetailPage } from './ThemeDetailPage.tsx'; -export { LayoutDetailPage } from './LayoutDetailPage.tsx'; -export { LayoutPreviewPage } from './LayoutPreviewPage.tsx'; -export type { Theme } from '../../types/theme.ts'; \ No newline at end of file diff --git a/src/components/ui/AlertDialog.css b/src/components/ui/AlertDialog.css new file mode 100644 index 0000000..702ebbc --- /dev/null +++ b/src/components/ui/AlertDialog.css @@ -0,0 +1,26 @@ +/* Alert Dialog Styles */ +.alert-dialog { + text-align: center; +} + +.alert-content { + margin-bottom: 2rem; +} + +.alert-icon { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.alert-message { + color: #374151; + font-size: 1rem; + line-height: 1.5; + margin: 0; +} + +.alert-actions { + display: flex; + justify-content: center; + gap: 0.75rem; +} \ No newline at end of file diff --git a/src/components/ui/AlertDialog.tsx b/src/components/ui/AlertDialog.tsx index e41b813..5329b54 100644 --- a/src/components/ui/AlertDialog.tsx +++ b/src/components/ui/AlertDialog.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { Modal } from './Modal.tsx'; +import { Button } from './buttons/Button.tsx'; +import './AlertDialog.css'; interface AlertDialogProps { isOpen: boolean; @@ -54,69 +56,14 @@ export const AlertDialog: React.FC = ({

{message}

- +
- - ); }; \ No newline at end of file diff --git a/src/components/ui/ConfirmDialog.css b/src/components/ui/ConfirmDialog.css new file mode 100644 index 0000000..6677baf --- /dev/null +++ b/src/components/ui/ConfirmDialog.css @@ -0,0 +1,27 @@ +/* Confirm Dialog Styles */ +.confirm-dialog { + text-align: center; +} + +.confirm-content { + margin-bottom: 2rem; +} + +.confirm-icon { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.confirm-message { + color: #374151; + font-size: 1rem; + line-height: 1.5; + margin: 0; + text-align: left; +} + +.confirm-actions { + display: flex; + justify-content: center; + gap: 0.75rem; +} \ No newline at end of file diff --git a/src/components/ui/ConfirmDialog.tsx b/src/components/ui/ConfirmDialog.tsx index 7de8c48..8480e8d 100644 --- a/src/components/ui/ConfirmDialog.tsx +++ b/src/components/ui/ConfirmDialog.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { Modal } from './Modal.tsx'; +import { Button } from './buttons/Button.tsx'; +import './ConfirmDialog.css'; interface ConfirmDialogProps { isOpen: boolean; @@ -80,100 +82,20 @@ export const ConfirmDialog: React.FC = ({

{message}

- - +
- - ); }; \ No newline at end of file diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index e8e9814..36109dc 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import { CloseButton } from './buttons/CloseButton.tsx'; import './Modal.css'; interface ModalProps { @@ -61,14 +62,12 @@ export const Modal: React.FC = ({ {title && (

{title}

- + title="Close modal" + />
)}
diff --git a/src/components/ui/buttons/ActionButton.css b/src/components/ui/buttons/ActionButton.css new file mode 100644 index 0000000..327357c --- /dev/null +++ b/src/components/ui/buttons/ActionButton.css @@ -0,0 +1,78 @@ +/* Action Button Styles */ +.action-button { + /* Layout */ + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + + /* Typography */ + font-family: inherit; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + white-space: nowrap; + + /* Interaction */ + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; + + /* Appearance */ + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + outline: none; +} + +.action-button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.action-button:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +/* Size Variants */ +.action-button.large { + padding: 1rem 2rem; + font-size: 1rem; + font-weight: 600; +} + +/* Primary Variant */ +.action-button.primary { + background: #3b82f6; + color: white; + border-color: #3b82f6; +} + +.action-button.primary:hover:not(:disabled) { + background: #2563eb; + border-color: #2563eb; +} + +.action-button.primary:active:not(:disabled) { + background: #1d4ed8; + border-color: #1d4ed8; +} + +/* Secondary Variant */ +.action-button.secondary { + background: #f8fafc; + color: #64748b; + border-color: #e2e8f0; +} + +.action-button.secondary:hover:not(:disabled) { + background: #f1f5f9; + color: #475569; + border-color: #cbd5e1; +} + +.action-button.secondary:active:not(:disabled) { + background: #e2e8f0; + color: #334155; +} \ No newline at end of file diff --git a/src/components/ui/buttons/ActionButton.tsx b/src/components/ui/buttons/ActionButton.tsx new file mode 100644 index 0000000..8a77fbb --- /dev/null +++ b/src/components/ui/buttons/ActionButton.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type { ReactNode } from 'react'; +import './ActionButton.css'; + +interface ActionButtonProps { + children: ReactNode; + variant?: 'primary' | 'secondary'; + size?: 'medium' | 'large'; + disabled?: boolean; + loading?: boolean; + onClick?: () => void; + type?: 'button' | 'submit' | 'reset'; + className?: string; + title?: string; +} + +export const ActionButton: React.FC = ({ + children, + variant = 'primary', + size = 'medium', + disabled = false, + loading = false, + onClick, + type = 'button', + className = '', + title, +}) => { + const baseClasses = 'action-button'; + const variantClass = variant; + const sizeClass = size === 'large' ? 'large' : ''; + + const classes = [ + baseClasses, + variantClass, + sizeClass, + className + ].filter(Boolean).join(' '); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/buttons/BackButton.css b/src/components/ui/buttons/BackButton.css new file mode 100644 index 0000000..f74b8ed --- /dev/null +++ b/src/components/ui/buttons/BackButton.css @@ -0,0 +1,43 @@ +/* Back Button Styles */ +.back-button { + /* Layout */ + display: inline-flex; + align-items: center; + gap: 0.25rem; + + /* Typography */ + font-family: inherit; + font-size: 0.875rem; + font-weight: 500; + color: #64748b; + + /* Interaction */ + background: none; + border: none; + cursor: pointer; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + transition: all 0.2s ease; + text-decoration: none; +} + +.back-button:hover:not(:disabled) { + background: #f1f5f9; + color: #334155; +} + +.back-button:active:not(:disabled) { + background: #e2e8f0; + color: #1e293b; +} + +.back-button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.back-button:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} \ No newline at end of file diff --git a/src/components/ui/buttons/BackButton.tsx b/src/components/ui/buttons/BackButton.tsx new file mode 100644 index 0000000..b0f66f8 --- /dev/null +++ b/src/components/ui/buttons/BackButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type { ReactNode } from 'react'; +import './BackButton.css'; + +interface BackButtonProps { + children?: ReactNode; + onClick?: () => void; + disabled?: boolean; + className?: string; + title?: string; +} + +export const BackButton: React.FC = ({ + children = '← Back', + onClick, + disabled = false, + className = '', + title, +}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/buttons/Button.css b/src/components/ui/buttons/Button.css new file mode 100644 index 0000000..d0a6fde --- /dev/null +++ b/src/components/ui/buttons/Button.css @@ -0,0 +1,144 @@ +/* Base Button Styles */ +.button { + /* Layout */ + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + + /* Typography */ + font-family: inherit; + font-weight: 500; + text-decoration: none; + white-space: nowrap; + + /* Interaction */ + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; + + /* Appearance */ + border-radius: 0.375rem; + outline: none; +} + +.button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +/* Size Variants */ +.button-small { + padding: 0.375rem 0.75rem; + font-size: 0.75rem; + border-radius: 0.25rem; +} + +.button-medium { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.button-large { + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 600; +} + +/* Primary Variant */ +.button-primary { + background: #3b82f6; + color: white; + border-color: #3b82f6; +} + +.button-primary:hover:not(:disabled) { + background: #2563eb; + border-color: #2563eb; +} + +.button-primary:active:not(:disabled) { + background: #1d4ed8; + border-color: #1d4ed8; +} + +/* Secondary Variant */ +.button-secondary { + background: #f8fafc; + color: #64748b; + border-color: #e2e8f0; +} + +.button-secondary:hover:not(:disabled) { + background: #f1f5f9; + color: #475569; + border-color: #cbd5e1; +} + +.button-secondary:active:not(:disabled) { + background: #e2e8f0; + color: #334155; +} + +/* Danger Variant */ +.button-danger { + background: #dc2626; + color: white; + border-color: #dc2626; +} + +.button-danger:hover:not(:disabled) { + background: #b91c1c; + border-color: #b91c1c; +} + +.button-danger:active:not(:disabled) { + background: #991b1b; + border-color: #991b1b; +} + +/* Link Variant */ +.button-link { + background: none; + color: #64748b; + border: none; + text-decoration: underline; + padding: 0.25rem 0.5rem; +} + +.button-link:hover:not(:disabled) { + color: #374151; +} + +.button-link:active:not(:disabled) { + color: #1f2937; +} + +/* Loading State */ +.button-loading { + position: relative; + color: transparent; +} + +.button-loading::after { + content: ''; + position: absolute; + width: 1rem; + height: 1rem; + border: 2px solid transparent; + border-top: 2px solid currentColor; + border-radius: 50%; + animation: button-spin 1s linear infinite; + color: inherit; +} + +@keyframes button-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/src/components/ui/buttons/Button.tsx b/src/components/ui/buttons/Button.tsx new file mode 100644 index 0000000..1976c71 --- /dev/null +++ b/src/components/ui/buttons/Button.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type { ReactNode } from 'react'; +import './Button.css'; + +interface ButtonProps { + children: ReactNode; + variant?: 'primary' | 'secondary' | 'danger' | 'link'; + size?: 'small' | 'medium' | 'large'; + disabled?: boolean; + loading?: boolean; + onClick?: () => void; + type?: 'button' | 'submit' | 'reset'; + className?: string; + title?: string; +} + +export const Button: React.FC = ({ + children, + variant = 'primary', + size = 'medium', + disabled = false, + loading = false, + onClick, + type = 'button', + className = '', + title, +}) => { + const baseClasses = 'button'; + const variantClass = `button-${variant}`; + const sizeClass = `button-${size}`; + const loadingClass = loading ? 'button-loading' : ''; + + const classes = [ + baseClasses, + variantClass, + sizeClass, + loadingClass, + className + ].filter(Boolean).join(' '); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/buttons/CancelLink.css b/src/components/ui/buttons/CancelLink.css new file mode 100644 index 0000000..476f765 --- /dev/null +++ b/src/components/ui/buttons/CancelLink.css @@ -0,0 +1,41 @@ +/* Cancel Link Styles */ +.cancel-link { + /* Layout */ + display: inline-flex; + align-items: center; + + /* Typography */ + font-family: inherit; + font-size: 0.875rem; + font-weight: 400; + color: #64748b; + text-decoration: underline; + + /* Interaction */ + background: none; + border: none; + cursor: pointer; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + transition: color 0.2s ease; +} + +.cancel-link:hover:not(:disabled) { + color: #374151; + text-decoration: underline; +} + +.cancel-link:active:not(:disabled) { + color: #1f2937; +} + +.cancel-link:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.cancel-link:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} \ No newline at end of file diff --git a/src/components/ui/buttons/CancelLink.tsx b/src/components/ui/buttons/CancelLink.tsx new file mode 100644 index 0000000..9374afb --- /dev/null +++ b/src/components/ui/buttons/CancelLink.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type { ReactNode } from 'react'; +import './CancelLink.css'; + +interface CancelLinkProps { + children: ReactNode; + onClick?: () => void; + disabled?: boolean; + className?: string; + title?: string; +} + +export const CancelLink: React.FC = ({ + children, + onClick, + disabled = false, + className = '', + title, +}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/buttons/CloseButton.css b/src/components/ui/buttons/CloseButton.css new file mode 100644 index 0000000..a34f792 --- /dev/null +++ b/src/components/ui/buttons/CloseButton.css @@ -0,0 +1,83 @@ +/* Close Button Styles */ +.close-button { + /* Layout */ + display: flex; + align-items: center; + justify-content: center; + + /* Typography */ + font-family: inherit; + font-weight: 500; + color: #374151; + + /* Interaction */ + background: rgba(255, 255, 255, 0.9); + border: none; + cursor: pointer; + transition: all 0.2s ease; + + /* Appearance */ + border-radius: 50%; + outline: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.close-button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.close-button:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +/* Size Variants */ +.close-button-small { + width: 28px; + height: 28px; + font-size: 0.875rem; +} + +.close-button-medium { + width: 36px; + height: 36px; + font-size: 1rem; +} + +.close-button-large { + width: 44px; + height: 44px; + font-size: 1.25rem; +} + +/* Style Variants */ +.close-button-default:hover:not(:disabled) { + background: rgba(255, 255, 255, 1); + transform: scale(1.05); +} + +.close-button-modal { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 10; +} + +.close-button-modal:hover:not(:disabled) { + background: rgba(255, 255, 255, 1); + transform: scale(1.05); +} + +.close-button-preview { + position: absolute; + top: 2rem; + right: 2rem; + z-index: 10; +} + +.close-button-preview:hover:not(:disabled) { + background: rgba(255, 255, 255, 1); + transform: scale(1.05); +} \ No newline at end of file diff --git a/src/components/ui/buttons/CloseButton.tsx b/src/components/ui/buttons/CloseButton.tsx new file mode 100644 index 0000000..cd7e7c8 --- /dev/null +++ b/src/components/ui/buttons/CloseButton.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import './CloseButton.css'; + +interface CloseButtonProps { + onClick?: () => void; + disabled?: boolean; + className?: string; + title?: string; + size?: 'small' | 'medium' | 'large'; + variant?: 'default' | 'modal' | 'preview'; +} + +export const CloseButton: React.FC = ({ + onClick, + disabled = false, + className = '', + title = 'Close', + size = 'medium', + variant = 'default', +}) => { + const baseClasses = 'close-button'; + const sizeClass = `close-button-${size}`; + const variantClass = `close-button-${variant}`; + + const classes = [ + baseClasses, + sizeClass, + variantClass, + className + ].filter(Boolean).join(' '); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/ui/buttons/NavigationButton.css b/src/components/ui/buttons/NavigationButton.css new file mode 100644 index 0000000..26af23a --- /dev/null +++ b/src/components/ui/buttons/NavigationButton.css @@ -0,0 +1,59 @@ +/* Navigation Button Styles */ +.nav-button { + /* Layout */ + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + + /* Typography */ + font-family: inherit; + font-size: 0.875rem; + font-weight: 500; + color: #64748b; + + /* Interaction */ + background: rgba(255, 255, 255, 0.9); + border: 1px solid #e2e8f0; + cursor: pointer; + transition: all 0.2s ease; + + /* Appearance */ + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + outline: none; + backdrop-filter: blur(8px); +} + +.nav-button:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.nav-button:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +.nav-button:hover:not(:disabled) { + background: rgba(255, 255, 255, 1); + color: #374151; + border-color: #cbd5e1; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.nav-button:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +/* Direction-specific styles */ +.nav-button-previous { + padding-left: 0.5rem; +} + +.nav-button-next { + padding-right: 0.5rem; +} \ No newline at end of file diff --git a/src/components/ui/buttons/NavigationButton.tsx b/src/components/ui/buttons/NavigationButton.tsx new file mode 100644 index 0000000..0c53e8a --- /dev/null +++ b/src/components/ui/buttons/NavigationButton.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type { ReactNode } from 'react'; +import './NavigationButton.css'; + +interface NavigationButtonProps { + children: ReactNode; + onClick?: () => void; + disabled?: boolean; + className?: string; + title?: string; + direction?: 'previous' | 'next' | 'none'; +} + +export const NavigationButton: React.FC = ({ + children, + onClick, + disabled = false, + className = '', + title, + direction = 'none', +}) => { + const baseClasses = 'nav-button'; + const directionClass = direction !== 'none' ? `nav-button-${direction}` : ''; + + const classes = [ + baseClasses, + directionClass, + className + ].filter(Boolean).join(' '); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/themes/index.ts b/src/themes/index.ts deleted file mode 100644 index e726b0a..0000000 --- a/src/themes/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Re-export from the theme loader utility -export { discoverThemes as getThemes, loadTheme } from '../utils/themeLoader.ts'; - -// Import for internal use -import { loadTheme } from '../utils/themeLoader.ts'; - -// Helper function to get a single theme by ID -export const getTheme = async (themeId: string) => { - return loadTheme(themeId, false); -}; \ No newline at end of file