Consolidate button components and eliminate barrel exports
## Button Component System - Create dedicated components/ui/buttons directory with 6 specialized button components: - Button.tsx: Universal button with variants (primary, secondary, danger, link) and sizes - ActionButton.tsx: Action buttons matching existing patterns - BackButton.tsx: Navigation back buttons - CancelLink.tsx: Cancel/link style buttons - CloseButton.tsx: Modal/preview close buttons with variants - NavigationButton.tsx: Presentation navigation buttons - Update key components to use new button system (SlideEditor, ContentEditor, Modal, AlertDialog, ConfirmDialog) - Replace inline styled-jsx with proper CSS files for AlertDialog and ConfirmDialog ## Barrel Export Elimination - Remove all barrel export files violating IMPORT_STANDARDS.md: - src/themes/index.ts - src/components/themes/index.ts - src/components/presentations/index.ts - src/components/slide-editor/index.ts - Update 15+ files to use direct imports from themeLoader.ts instead of barrel exports - Fix function naming conflict in ThemeDetailPage.tsx (loadTheme shadowing) - Follow project standards: direct imports with .tsx extensions for better Vite performance ## Benefits - Improved Vite tree shaking and module resolution performance - Consistent, reusable button system across application - Adherence to project coding standards and import conventions - Reduced bundle size through elimination of barrel export overhead 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
127b0fe96a
commit
b4b61ad761
@ -12,5 +12,5 @@
|
||||
"hasMasterSlide": true
|
||||
}
|
||||
},
|
||||
"generated": "2025-08-21T16:18:27.749Z"
|
||||
"generated": "2025-08-21T16:40:45.572Z"
|
||||
}
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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';
|
@ -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<ContentEditorProps> = ({
|
||||
|
||||
<div className="content-actions">
|
||||
<div className="action-links">
|
||||
<button
|
||||
type="button"
|
||||
className="cancel-link"
|
||||
<CancelLink
|
||||
onClick={onCancel}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel editing
|
||||
</button>
|
||||
</CancelLink>
|
||||
</div>
|
||||
<div className="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button primary"
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : (isEditingExisting ? 'Update Slide' : 'Save Slide')}
|
||||
</button>
|
||||
{isEditingExisting ? 'Update Slide' : 'Save Slide'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 = () => {
|
||||
|
||||
<div className="editor-actions">
|
||||
{currentStep === 'content' && selectedLayout && (
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
<ActionButton
|
||||
variant="secondary"
|
||||
onClick={() => setShowPreview(true)}
|
||||
>
|
||||
Full Preview
|
||||
</button>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
@ -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<SlidePreviewModalProps> = ({
|
||||
)}
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
className="preview-close-button"
|
||||
<CloseButton
|
||||
size="large"
|
||||
variant="preview"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
title="Close preview (ESC)"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
/>
|
||||
|
||||
{/* Theme info */}
|
||||
<div className="preview-info">
|
||||
|
@ -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';
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 = () => {
|
||||
|
@ -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<string | null>(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) {
|
||||
|
@ -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';
|
26
src/components/ui/AlertDialog.css
Normal file
26
src/components/ui/AlertDialog.css
Normal file
@ -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;
|
||||
}
|
@ -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<AlertDialogProps> = ({
|
||||
<p className="alert-message">{message}</p>
|
||||
</div>
|
||||
<div className="alert-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="button primary"
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onClose}
|
||||
autoFocus
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.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;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.button.primary:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
`}</style>
|
||||
</Modal>
|
||||
);
|
||||
};
|
27
src/components/ui/ConfirmDialog.css
Normal file
27
src/components/ui/ConfirmDialog.css
Normal file
@ -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;
|
||||
}
|
@ -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<ConfirmDialogProps> = ({
|
||||
<p className="confirm-message">{message}</p>
|
||||
</div>
|
||||
<div className="confirm-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="button secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`button ${isDestructive || type === 'danger' ? 'danger' : 'primary'}`}
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDestructive || type === 'danger' ? 'danger' : 'primary'}
|
||||
onClick={handleConfirm}
|
||||
autoFocus
|
||||
>
|
||||
{getConfirmText()}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.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;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.danger:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.button.danger:focus {
|
||||
outline-color: #dc2626;
|
||||
}
|
||||
`}</style>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -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<ModalProps> = ({
|
||||
{title && (
|
||||
<header className="modal-header">
|
||||
<h2 className="modal-title">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="modal-close-button"
|
||||
<CloseButton
|
||||
size="medium"
|
||||
variant="modal"
|
||||
onClick={onClose}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
title="Close modal"
|
||||
/>
|
||||
</header>
|
||||
)}
|
||||
<div className="modal-body">
|
||||
|
78
src/components/ui/buttons/ActionButton.css
Normal file
78
src/components/ui/buttons/ActionButton.css
Normal file
@ -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;
|
||||
}
|
50
src/components/ui/buttons/ActionButton.tsx
Normal file
50
src/components/ui/buttons/ActionButton.tsx
Normal file
@ -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<ActionButtonProps> = ({
|
||||
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 (
|
||||
<button
|
||||
type={type}
|
||||
className={classes}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
>
|
||||
{loading ? 'Loading...' : children}
|
||||
</button>
|
||||
);
|
||||
};
|
43
src/components/ui/buttons/BackButton.css
Normal file
43
src/components/ui/buttons/BackButton.css
Normal file
@ -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;
|
||||
}
|
31
src/components/ui/buttons/BackButton.tsx
Normal file
31
src/components/ui/buttons/BackButton.tsx
Normal file
@ -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<BackButtonProps> = ({
|
||||
children = '← Back',
|
||||
onClick,
|
||||
disabled = false,
|
||||
className = '',
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`back-button ${className}`.trim()}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
144
src/components/ui/buttons/Button.css
Normal file
144
src/components/ui/buttons/Button.css
Normal file
@ -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); }
|
||||
}
|
52
src/components/ui/buttons/Button.tsx
Normal file
52
src/components/ui/buttons/Button.tsx
Normal file
@ -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<ButtonProps> = ({
|
||||
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 (
|
||||
<button
|
||||
type={type}
|
||||
className={classes}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
>
|
||||
{loading ? 'Loading...' : children}
|
||||
</button>
|
||||
);
|
||||
};
|
41
src/components/ui/buttons/CancelLink.css
Normal file
41
src/components/ui/buttons/CancelLink.css
Normal file
@ -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;
|
||||
}
|
31
src/components/ui/buttons/CancelLink.tsx
Normal file
31
src/components/ui/buttons/CancelLink.tsx
Normal file
@ -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<CancelLinkProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className = '',
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`cancel-link ${className}`.trim()}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
83
src/components/ui/buttons/CloseButton.css
Normal file
83
src/components/ui/buttons/CloseButton.css
Normal file
@ -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);
|
||||
}
|
43
src/components/ui/buttons/CloseButton.tsx
Normal file
43
src/components/ui/buttons/CloseButton.tsx
Normal file
@ -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<CloseButtonProps> = ({
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
);
|
||||
};
|
59
src/components/ui/buttons/NavigationButton.css
Normal file
59
src/components/ui/buttons/NavigationButton.css
Normal file
@ -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;
|
||||
}
|
42
src/components/ui/buttons/NavigationButton.tsx
Normal file
42
src/components/ui/buttons/NavigationButton.tsx
Normal file
@ -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<NavigationButtonProps> = ({
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
@ -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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user