progress
This commit is contained in:
parent
86b90ea977
commit
8b40e9332f
|
|
@ -0,0 +1,12 @@
|
|||
Mock Data File
|
||||
==============
|
||||
|
||||
This is a sample text file containing mock data for testing purposes.
|
||||
|
||||
Sample entries:
|
||||
- Entry 1: Lorem ipsum dolor sit amet
|
||||
- Entry 2: Consectetur adipiscing elit
|
||||
- Entry 3: Sed do eiusmod tempor incididunt
|
||||
|
||||
Generated at: 2025-01-06
|
||||
Purpose: Testing and development
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
# Practical UI Component Enhancement Specification
|
||||
|
||||
## Core Challenge
|
||||
Take an existing UI component and create a **significantly enhanced version** that users immediately recognize as superior. Focus on practical improvements through animation, interaction design, and intelligent behaviors that make the component more delightful and efficient to use.
|
||||
|
||||
## Output Requirements
|
||||
|
||||
**File Naming**: `ui_enhanced_[iteration_number].html`
|
||||
|
||||
**Content Structure**: Clean, focused HTML file showcasing the enhanced component
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>[Component Name] Enhanced</title>
|
||||
<style>
|
||||
/* Modern, polished styling with smooth animations */
|
||||
/* Focus on the enhanced component - minimal page chrome */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>[Component Type] - Enhanced</h1>
|
||||
|
||||
<!-- The enhanced component in action -->
|
||||
<div class="component-showcase">
|
||||
<!-- Enhanced component implementation -->
|
||||
<!-- Include realistic demo content to show it working -->
|
||||
</div>
|
||||
|
||||
<!-- Optional: Additional examples if needed to demonstrate features -->
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Smooth, performant JavaScript
|
||||
// Focus on enhanced interactions and smart behaviors
|
||||
// Include accessibility features and keyboard navigation
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Enhancement Principles
|
||||
|
||||
### **Practical Improvements**
|
||||
- **Animation & Polish**: Smooth transitions, hover effects, loading states
|
||||
- **Micro-Interactions**: Delightful feedback for user actions
|
||||
- **Smart Behaviors**: Auto-focus, intelligent defaults, predictive features
|
||||
- **Better Feedback**: Clear visual responses, progress indicators, validation
|
||||
- **Enhanced Accessibility**: Superior keyboard navigation, screen reader support
|
||||
- **Performance**: 60fps animations, efficient event handling, fast responses
|
||||
|
||||
### **Self-Evident Value**
|
||||
- Users should immediately see the improvement without explanation
|
||||
- Enhanced version should feel significantly better to interact with
|
||||
- Improvements should be intuitive and build on familiar patterns
|
||||
- Component should invite exploration and repeated use
|
||||
|
||||
### **Focus Areas**
|
||||
- **Visual Excellence**: Better typography, spacing, colors, shadows, gradients
|
||||
- **Interaction Flow**: Smoother state changes, better click targets, drag interactions
|
||||
- **Intelligence**: Smart validation, auto-complete, contextual suggestions
|
||||
- **Responsiveness**: Adapts to screen size, input method, user preferences
|
||||
- **Error Handling**: Graceful error states, helpful constraints, recovery options
|
||||
|
||||
## Enhancement Dimensions
|
||||
|
||||
### **Animation & Motion**
|
||||
- **State Transitions**: Smooth changes between states (hover, focus, active, disabled)
|
||||
- **Loading States**: Elegant spinners, skeleton screens, progressive loading
|
||||
- **Micro-Animations**: Button press feedback, input focus, menu reveals
|
||||
- **Physics**: Natural easing, spring animations, momentum-based interactions
|
||||
- **Performance**: Hardware-accelerated transforms, efficient animations
|
||||
|
||||
### **Interaction Intelligence**
|
||||
- **Predictive Behavior**: Auto-complete, smart suggestions, learned preferences
|
||||
- **Context Awareness**: Adapts based on user patterns, device capabilities
|
||||
- **Smart Defaults**: Intelligent initial values, remembered settings
|
||||
- **Progressive Disclosure**: Reveal complexity gradually as needed
|
||||
- **Shortcuts**: Keyboard shortcuts, gesture support, power-user features
|
||||
|
||||
### **Visual & Spatial Design**
|
||||
- **Depth & Layering**: Subtle shadows, elevation, z-index management
|
||||
- **Typography**: Better font choices, line spacing, reading experience
|
||||
- **Color & Contrast**: Accessible color palettes, dark mode support
|
||||
- **Spacing & Rhythm**: Consistent grid systems, harmonious proportions
|
||||
- **Iconography**: Clear, consistent icons with proper sizing
|
||||
|
||||
### **Accessibility & Inclusion**
|
||||
- **Keyboard Navigation**: Full keyboard support, logical tab order, shortcuts
|
||||
- **Screen Readers**: Proper ARIA labels, live regions, semantic markup
|
||||
- **Color Independence**: Information not conveyed by color alone
|
||||
- **Motion Sensitivity**: Respects prefers-reduced-motion settings
|
||||
- **Touch Targets**: Proper sizing for mobile, accessible click areas
|
||||
|
||||
## Target Components
|
||||
|
||||
### **Form Elements**
|
||||
- **Text Inputs**: Floating labels, smart validation, auto-complete
|
||||
- **Dropdowns**: Searchable, keyboard navigation, smart positioning
|
||||
- **Checkboxes/Radios**: Better visual feedback, group interactions
|
||||
- **File Uploads**: Drag-and-drop, progress indicators, preview
|
||||
- **Date Pickers**: Intuitive navigation, keyboard shortcuts, range selection
|
||||
|
||||
### **Navigation**
|
||||
- **Tabs**: Smooth transitions, overflow handling, keyboard navigation
|
||||
- **Menus**: Smart positioning, search, hierarchical navigation
|
||||
- **Breadcrumbs**: Interactive, collapsible, contextual actions
|
||||
- **Pagination**: Smart loading, infinite scroll, jump navigation
|
||||
|
||||
### **Data Display**
|
||||
- **Tables**: Sortable headers, row selection, responsive layouts
|
||||
- **Cards**: Hover states, action overlays, smart layouts
|
||||
- **Lists**: Virtual scrolling, drag reordering, bulk actions
|
||||
- **Charts**: Interactive tooltips, smooth animations, accessibility
|
||||
|
||||
### **Feedback & Status**
|
||||
- **Buttons**: Loading states, success feedback, smart disabled states
|
||||
- **Progress Bars**: Smooth animations, contextual information
|
||||
- **Alerts/Toasts**: Better positioning, dismissal, action buttons
|
||||
- **Modals**: Smooth entrance, focus management, backdrop interactions
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### **Enhancement Metrics**
|
||||
- **Usability**: Measurably easier/faster to use than standard version
|
||||
- **Delight**: Creates positive emotional response and satisfaction
|
||||
- **Accessibility**: WCAG 2.1 AA compliant with enhanced keyboard support
|
||||
- **Performance**: Smooth 60fps animations, fast response times
|
||||
- **Polish**: Professional visual design that feels modern and refined
|
||||
|
||||
### **Technical Excellence**
|
||||
- **Clean Code**: Well-structured, commented, maintainable
|
||||
- **Progressive Enhancement**: Works without JavaScript, enhanced with it
|
||||
- **Cross-Browser**: Compatible with modern browsers
|
||||
- **Responsive**: Adapts beautifully to different screen sizes
|
||||
- **Performant**: Efficient animations, minimal reflows, optimized assets
|
||||
|
||||
## Evolution Strategy
|
||||
|
||||
### **Iteration Progression**
|
||||
- **Foundation (1-3)**: Core component types with fundamental enhancements
|
||||
- **Sophistication (4-8)**: Advanced interactions, smart behaviors, complex animations
|
||||
- **Innovation (9+)**: Novel interaction patterns, AI-enhanced features, experimental UI
|
||||
|
||||
### **Enhancement Complexity**
|
||||
- **Visual Polish**: Start with better styling, colors, typography
|
||||
- **Interaction Layer**: Add smooth animations, hover effects, transitions
|
||||
- **Intelligence Layer**: Implement smart behaviors, prediction, adaptation
|
||||
- **Advanced Features**: Complex animations, gesture support, voice interaction
|
||||
|
||||
## Native Web Technologies
|
||||
|
||||
### **CSS Features**
|
||||
- **Modern Layout**: Grid, Flexbox, Container Queries
|
||||
- **Animations**: Transitions, Keyframes, Web Animations API
|
||||
- **Visual Effects**: Filters, Backdrop-filter, Clip-path, Gradients
|
||||
- **Responsive**: Media queries, Viewport units, Clamp functions
|
||||
- **Custom Properties**: Dynamic theming, animation coordination
|
||||
|
||||
### **JavaScript APIs**
|
||||
- **Interaction**: Intersection Observer, Resize Observer, Pointer Events
|
||||
- **Animation**: requestAnimationFrame, Web Animations API
|
||||
- **Accessibility**: Focus management, ARIA live regions
|
||||
- **Performance**: Virtual scrolling, Debouncing, Throttling
|
||||
- **Modern Features**: ES6+ syntax, Async/await, Template literals
|
||||
|
||||
## Ultra-Thinking Directive
|
||||
|
||||
Before each enhancement, deeply consider:
|
||||
|
||||
**User Experience:**
|
||||
- What frustrates users about the current component?
|
||||
- How can animation make interactions feel more natural?
|
||||
- What would make this component delightful to use repeatedly?
|
||||
- How can we anticipate and prevent user errors?
|
||||
|
||||
**Enhancement Opportunities:**
|
||||
- Which micro-interactions would provide the most value?
|
||||
- How can we make the component more accessible without sacrificing design?
|
||||
- What smart behaviors would users appreciate but not expect?
|
||||
- How can animation guide user attention and understanding?
|
||||
|
||||
**Technical Excellence:**
|
||||
- What modern web APIs can enhance this component?
|
||||
- How can we ensure smooth performance across devices?
|
||||
- What's the most elegant way to implement this enhancement?
|
||||
- How can we make the code maintainable and extensible?
|
||||
|
||||
**Practical Impact:**
|
||||
- Would users prefer this enhanced version in real applications?
|
||||
- How does this enhancement scale to enterprise use cases?
|
||||
- What's the learning curve vs. value proposition?
|
||||
- How can we make the enhancement feel familiar yet superior?
|
||||
|
||||
**Generate components that are:**
|
||||
- **Immediately Better**: Users instantly recognize the improvement
|
||||
- **Practically Useful**: Solves real problems with existing components
|
||||
- **Delightfully Animated**: Smooth, purposeful motion that enhances usability
|
||||
- **Accessible by Design**: Enhanced keyboard navigation and screen reader support
|
||||
- **Self-Contained**: No dependencies, works perfectly as a standalone HTML file
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,717 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Button Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f7fa;
|
||||
padding: 40px 20px;
|
||||
line-height: 1.6;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
color: #1a1a1a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.component-showcase {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.button-group h3 {
|
||||
font-size: 1.1rem;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Enhanced Button Base Styles */
|
||||
.btn {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-style: preserve-3d;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Ripple Effect Container */
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
/* Button Variants */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* Active State */
|
||||
.btn:active:not(:disabled) {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
/* Focus State */
|
||||
.btn:focus-visible {
|
||||
outline: 3px solid currentColor;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Disabled State */
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn-loading {
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-loading .btn-text {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.btn-loading .spinner {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Success Animation */
|
||||
.btn-success-state {
|
||||
background: #22c55e !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.btn-success-state .success-icon {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-success-state .btn-text {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Error Animation */
|
||||
.btn-error {
|
||||
animation: shake 0.5s;
|
||||
background: #ef4444 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
/* Icon Support */
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.btn-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.btn-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.btn-loading-progress .btn-progress {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-progress-bar {
|
||||
height: 100%;
|
||||
background: white;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Button Groups */
|
||||
.btn-group-horizontal {
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:first-child {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:last-child {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:not(:last-child) {
|
||||
border-right: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.8);
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: rgba(0,0,0,0.9);
|
||||
}
|
||||
|
||||
.btn:hover .tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
/* Demo Section */
|
||||
.demo-section {
|
||||
margin-top: 40px;
|
||||
padding-top: 40px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Mobile Optimization */
|
||||
@media (max-width: 768px) {
|
||||
.button-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-group-horizontal {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn {
|
||||
border-radius: 0;
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.btn {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Button - Enhanced</h1>
|
||||
|
||||
<div class="component-showcase">
|
||||
<div class="button-grid">
|
||||
<!-- Primary Buttons -->
|
||||
<div class="button-group">
|
||||
<h3>Primary Actions</h3>
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Click Me</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary" onclick="handleProgressClick(this)">
|
||||
<span class="btn-text">Upload File</span>
|
||||
<div class="spinner"></div>
|
||||
<div class="btn-progress">
|
||||
<div class="btn-progress-bar"></div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary" disabled>
|
||||
<span class="btn-text">Disabled</span>
|
||||
<span class="tooltip">Complete form to enable</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Buttons -->
|
||||
<div class="button-group">
|
||||
<h3>Secondary Actions</h3>
|
||||
<button class="btn btn-secondary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Save Draft</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary">
|
||||
<span class="btn-icon">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="btn-text">Edit</span>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary">
|
||||
<span class="btn-icon">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="btn-text">Add Item</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Success/Danger Buttons -->
|
||||
<div class="button-group">
|
||||
<h3>Contextual Actions</h3>
|
||||
<button class="btn btn-success" onclick="handleClick(this)">
|
||||
<span class="btn-text">Confirm</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" onclick="handleErrorClick(this)">
|
||||
<span class="btn-text">Delete</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" disabled>
|
||||
<span class="btn-text">Permanently Delete</span>
|
||||
<span class="tooltip">Action cannot be undone</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Groups -->
|
||||
<div class="demo-section">
|
||||
<h3 style="margin-bottom: 20px;">Button Groups</h3>
|
||||
|
||||
<div class="btn-group-horizontal">
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Previous</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Current</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Next</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<div class="demo-section">
|
||||
<h3 style="margin-bottom: 20px;">Interactive Demo</h3>
|
||||
<div class="demo-controls">
|
||||
<button class="btn btn-primary" onclick="simulateLoading(this)">
|
||||
<span class="btn-text">Simulate Loading</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" onclick="simulateProgress(this)">
|
||||
<span class="btn-text">Simulate Progress</span>
|
||||
<div class="spinner"></div>
|
||||
<div class="btn-progress">
|
||||
<div class="btn-progress-bar"></div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" onclick="simulateError(this)">
|
||||
<span class="btn-text">Simulate Error</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Ripple effect for all buttons
|
||||
document.querySelectorAll('.btn').forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
const ripple = document.createElement('span');
|
||||
const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2;
|
||||
const y = e.clientY - rect.top - size / 2;
|
||||
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
top: ${y}px;
|
||||
left: ${x}px;
|
||||
pointer-events: none;
|
||||
transform: scale(0);
|
||||
animation: ripple 0.6s ease-out;
|
||||
`;
|
||||
|
||||
this.appendChild(ripple);
|
||||
|
||||
setTimeout(() => ripple.remove(), 600);
|
||||
});
|
||||
});
|
||||
|
||||
// Add ripple animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Basic click handler with loading state
|
||||
function handleClick(button) {
|
||||
if (button.classList.contains('btn-loading')) return;
|
||||
|
||||
button.classList.add('btn-loading');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-loading');
|
||||
button.classList.add('btn-success-state');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-success-state');
|
||||
}, 1500);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Progress click handler
|
||||
function handleProgressClick(button) {
|
||||
if (button.classList.contains('btn-loading')) return;
|
||||
|
||||
button.classList.add('btn-loading', 'btn-loading-progress');
|
||||
const progressBar = button.querySelector('.btn-progress-bar');
|
||||
let progress = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 20;
|
||||
if (progress > 100) progress = 100;
|
||||
|
||||
progressBar.style.width = progress + '%';
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-loading', 'btn-loading-progress');
|
||||
button.classList.add('btn-success-state');
|
||||
progressBar.style.width = '0%';
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-success-state');
|
||||
}, 1500);
|
||||
}, 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Error click handler
|
||||
function handleErrorClick(button) {
|
||||
if (button.classList.contains('btn-loading')) return;
|
||||
|
||||
button.classList.add('btn-loading');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-loading');
|
||||
button.classList.add('btn-error');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-error');
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Demo functions
|
||||
function simulateLoading(button) {
|
||||
handleClick(button);
|
||||
}
|
||||
|
||||
function simulateProgress(button) {
|
||||
handleProgressClick(button);
|
||||
}
|
||||
|
||||
function simulateError(button) {
|
||||
handleErrorClick(button);
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && activeElement.classList.contains('btn')) {
|
||||
e.preventDefault();
|
||||
activeElement.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Touch feedback enhancement
|
||||
let touchTimeout;
|
||||
document.querySelectorAll('.btn').forEach(button => {
|
||||
button.addEventListener('touchstart', function() {
|
||||
this.style.transform = 'scale(0.95)';
|
||||
clearTimeout(touchTimeout);
|
||||
});
|
||||
|
||||
button.addEventListener('touchend', function() {
|
||||
touchTimeout = setTimeout(() => {
|
||||
this.style.transform = '';
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// Accessibility announcements
|
||||
const announcer = document.createElement('div');
|
||||
announcer.setAttribute('role', 'status');
|
||||
announcer.setAttribute('aria-live', 'polite');
|
||||
announcer.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
|
||||
document.body.appendChild(announcer);
|
||||
|
||||
function announce(message) {
|
||||
announcer.textContent = message;
|
||||
setTimeout(() => announcer.textContent = '', 1000);
|
||||
}
|
||||
|
||||
// Enhance button clicks with announcements
|
||||
const originalHandleClick = window.handleClick;
|
||||
window.handleClick = function(button) {
|
||||
originalHandleClick(button);
|
||||
announce('Processing...');
|
||||
setTimeout(() => announce('Action completed successfully'), 1500);
|
||||
};
|
||||
|
||||
// Focus management for button groups
|
||||
document.querySelectorAll('.btn-group-horizontal').forEach(group => {
|
||||
const buttons = group.querySelectorAll('.btn');
|
||||
|
||||
buttons.forEach((btn, index) => {
|
||||
btn.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight' && index < buttons.length - 1) {
|
||||
e.preventDefault();
|
||||
buttons[index + 1].focus();
|
||||
} else if (e.key === 'ArrowLeft' && index > 0) {
|
||||
e.preventDefault();
|
||||
buttons[index - 1].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,882 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cards Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-light: #818cf8;
|
||||
--primary-dark: #4f46e5;
|
||||
--secondary: #ec4899;
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--gray-900: #111827;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--radius: 12px;
|
||||
--radius-lg: 16px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--gray-900);
|
||||
background: var(--gray-50);
|
||||
padding: 2rem 1rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--gray-600);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Card Grid */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Base Card Styles */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: var(--transition);
|
||||
transform-style: preserve-3d;
|
||||
cursor: pointer;
|
||||
will-change: transform, box-shadow;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
outline: 3px solid var(--primary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
/* Card Image with Lazy Loading */
|
||||
.card-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), filter 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-image.loading::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.card-image img.blur-up {
|
||||
filter: blur(20px);
|
||||
}
|
||||
|
||||
.card-image img.loaded {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* Card Content */
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: var(--gray-600);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* Action Overlay */
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card:hover .card-actions,
|
||||
.card:focus-within .card-actions {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: white;
|
||||
transform: scale(1.1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: var(--gray-700);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.action-btn.liked svg {
|
||||
fill: var(--danger);
|
||||
stroke: var(--danger);
|
||||
}
|
||||
|
||||
/* Flip Card */
|
||||
.flip-card {
|
||||
perspective: 1000px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.flip-card-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
transition: transform 0.6s;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.flip-card.flipped .flip-card-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-card-front,
|
||||
.flip-card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flip-card-back {
|
||||
transform: rotateY(180deg);
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.flip-trigger {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition-fast);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.flip-trigger:hover {
|
||||
transform: scale(1.1);
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* Expandable Card */
|
||||
.expandable-card {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.expandable-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.expandable-card.expanded .expandable-content {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.expand-trigger {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: var(--transition-fast);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.expand-trigger:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.expand-trigger svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.expandable-card.expanded .expand-trigger svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Loading Skeleton */
|
||||
.card-skeleton {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--gray-200) 25%, var(--gray-100) 50%, var(--gray-200) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-image {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.skeleton-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
height: 24px;
|
||||
width: 70%;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 16px;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-text:last-child {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* Selection Mode */
|
||||
.selection-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.selection-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selection-count {
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selection-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--gray-300);
|
||||
}
|
||||
|
||||
/* Mobile Touch Support */
|
||||
@media (max-width: 768px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
touch-action: pan-y;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card.swiping {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus Styles */
|
||||
.card:focus-visible,
|
||||
.action-btn:focus-visible,
|
||||
.flip-trigger:focus-visible,
|
||||
.expand-trigger:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--gray-300);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Cards - Enhanced</h1>
|
||||
<p class="subtitle">Beautiful, interactive cards with smooth animations and intelligent behaviors</p>
|
||||
|
||||
<!-- Selection Mode -->
|
||||
<div class="selection-mode" id="selectionMode" style="display: none;">
|
||||
<input type="checkbox" class="selection-checkbox" id="selectAll">
|
||||
<span class="selection-count">0 selected</span>
|
||||
<div class="selection-actions">
|
||||
<button class="btn btn-primary">Share</button>
|
||||
<button class="btn btn-secondary">Archive</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Grid -->
|
||||
<div class="card-grid" id="cardGrid">
|
||||
<!-- Loading Skeletons -->
|
||||
<div class="card card-skeleton">
|
||||
<div class="skeleton skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-skeleton">
|
||||
<div class="skeleton skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-skeleton">
|
||||
<div class="skeleton skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Special Cards Section -->
|
||||
<h2 style="margin: 3rem 0 1.5rem; font-size: 1.875rem; font-weight: 700;">Special Card Types</h2>
|
||||
|
||||
<div class="card-grid">
|
||||
<!-- Flip Card -->
|
||||
<div class="flip-card" id="flipCard">
|
||||
<div class="flip-card-inner">
|
||||
<div class="flip-card-front card">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='200'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%234f46e5;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23ec4899;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='200' fill='url(%23g)'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='white' font-family='Arial' font-size='24' font-weight='bold'%3EFlip Me%3C/text%3E%3C/svg%3E" alt="Flip card front">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">Interactive Flip Card</h3>
|
||||
<p class="card-description">Click the button to reveal additional information on the back of this card.</p>
|
||||
<button class="flip-trigger" aria-label="Flip card">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flip-card-back">
|
||||
<h3 style="font-size: 1.5rem; margin-bottom: 1rem;">Additional Details</h3>
|
||||
<p style="margin-bottom: 1.5rem;">This is the back of the card with more detailed information that was hidden from the initial view.</p>
|
||||
<ul style="text-align: left; margin-bottom: 1.5rem;">
|
||||
<li>Hidden feature #1</li>
|
||||
<li>Secret detail #2</li>
|
||||
<li>Extra information #3</li>
|
||||
</ul>
|
||||
<button class="btn" style="background: white; color: var(--primary);" onclick="document.getElementById('flipCard').classList.remove('flipped')">
|
||||
Flip Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expandable Card -->
|
||||
<div class="card expandable-card" id="expandableCard">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='200'%3E%3Cdefs%3E%3ClinearGradient id='g2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2310b981;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%236366f1;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='200' fill='url(%23g2)'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='white' font-family='Arial' font-size='24' font-weight='bold'%3EExpandable%3C/text%3E%3C/svg%3E" alt="Expandable card">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">Expandable Content Card</h3>
|
||||
<p class="card-description">This card can expand to show additional content with a smooth animation.</p>
|
||||
<button class="expand-trigger">
|
||||
<span>Show more</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="expandable-content">
|
||||
<div style="padding-top: 1rem; border-top: 1px solid var(--gray-200);">
|
||||
<h4 style="font-weight: 600; margin-bottom: 0.5rem;">Extended Information</h4>
|
||||
<p style="color: var(--gray-600); margin-bottom: 1rem;">Here's the additional content that was hidden. It smoothly animates into view when expanded.</p>
|
||||
<ul style="color: var(--gray-600); padding-left: 1.5rem;">
|
||||
<li>Extra detail that wasn't visible before</li>
|
||||
<li>Another piece of hidden information</li>
|
||||
<li>More content to explore</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="action-btn" aria-label="Like">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" aria-label="Share">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||
<polyline points="16 6 12 2 8 6"/>
|
||||
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Card data
|
||||
const cardData = [
|
||||
{
|
||||
title: "Mountain Vista",
|
||||
description: "Experience breathtaking views from the mountain peaks with crystal clear skies.",
|
||||
image: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=200&fit=crop",
|
||||
author: "Alex Chen",
|
||||
date: "2 days ago"
|
||||
},
|
||||
{
|
||||
title: "Ocean Breeze",
|
||||
description: "Feel the calming waves and gentle sea breeze at this pristine beach location.",
|
||||
image: "https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=400&h=200&fit=crop",
|
||||
author: "Sarah Miller",
|
||||
date: "1 week ago"
|
||||
},
|
||||
{
|
||||
title: "Forest Trail",
|
||||
description: "Discover hidden paths through ancient forests filled with wildlife and wonder.",
|
||||
image: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400&h=200&fit=crop",
|
||||
author: "David Park",
|
||||
date: "3 days ago"
|
||||
},
|
||||
{
|
||||
title: "Desert Sunset",
|
||||
description: "Watch the sun paint the desert landscape in brilliant shades of orange and red.",
|
||||
image: "https://images.unsplash.com/photo-1509316785289-025f5b846b35?w=400&h=200&fit=crop",
|
||||
author: "Maria Garcia",
|
||||
date: "5 days ago"
|
||||
},
|
||||
{
|
||||
title: "City Lights",
|
||||
description: "Experience the vibrant energy of the city as it comes alive after dark.",
|
||||
image: "https://images.unsplash.com/photo-1514565131-fce0801e5785?w=400&h=200&fit=crop",
|
||||
author: "James Wilson",
|
||||
date: "1 day ago"
|
||||
},
|
||||
{
|
||||
title: "Lakeside Calm",
|
||||
description: "Find peace and tranquility by the still waters of this mountain lake.",
|
||||
image: "https://images.unsplash.com/photo-1439066615861-d1af74d74000?w=400&h=200&fit=crop",
|
||||
author: "Emma Thompson",
|
||||
date: "4 days ago"
|
||||
}
|
||||
];
|
||||
|
||||
// Create card HTML
|
||||
function createCard(data, index) {
|
||||
return `
|
||||
<div class="card" data-index="${index}" tabindex="0" role="article" aria-label="${data.title}">
|
||||
<div class="card-image loading">
|
||||
<img
|
||||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Crect width='20' height='10' fill='%23e5e7eb'/%3E%3C/svg%3E"
|
||||
data-src="${data.image}"
|
||||
alt="${data.title}"
|
||||
class="blur-up"
|
||||
>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">${data.title}</h3>
|
||||
<p class="card-description">${data.description}</p>
|
||||
<div class="card-meta">
|
||||
<span>${data.author}</span>
|
||||
<span>${data.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="action-btn like-btn" aria-label="Like ${data.title}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" aria-label="Share ${data.title}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||
<polyline points="16 6 12 2 8 6"/>
|
||||
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" aria-label="More options for ${data.title}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="12" cy="5" r="1"/>
|
||||
<circle cx="12" cy="19" r="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Initialize cards
|
||||
const cardGrid = document.getElementById('cardGrid');
|
||||
setTimeout(() => {
|
||||
cardGrid.innerHTML = cardData.map((data, index) => createCard(data, index)).join('');
|
||||
initializeCards();
|
||||
}, 1500);
|
||||
|
||||
// Initialize card functionality
|
||||
function initializeCards() {
|
||||
// Lazy loading images
|
||||
const images = document.querySelectorAll('.card-image img[data-src]');
|
||||
const imageObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
const parent = img.parentElement;
|
||||
|
||||
// Load high-res image
|
||||
const highResImg = new Image();
|
||||
highResImg.src = img.dataset.src;
|
||||
highResImg.onload = () => {
|
||||
img.src = highResImg.src;
|
||||
setTimeout(() => {
|
||||
img.classList.add('loaded');
|
||||
parent.classList.remove('loading');
|
||||
}, 50);
|
||||
};
|
||||
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach(img => imageObserver.observe(img));
|
||||
|
||||
// Like button functionality
|
||||
document.querySelectorAll('.like-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
btn.classList.toggle('liked');
|
||||
|
||||
// Haptic feedback on mobile
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate(10);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Card selection
|
||||
let selectedCards = new Set();
|
||||
let selectionMode = false;
|
||||
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.action-btn') || e.target.closest('.flip-trigger') || e.target.closest('.expand-trigger')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey || selectionMode) {
|
||||
const index = card.dataset.index;
|
||||
if (selectedCards.has(index)) {
|
||||
selectedCards.delete(index);
|
||||
card.classList.remove('selected');
|
||||
} else {
|
||||
selectedCards.add(index);
|
||||
card.classList.add('selected');
|
||||
}
|
||||
updateSelectionMode();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
card.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
card.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Selection mode controls
|
||||
function updateSelectionMode() {
|
||||
const selectionModeEl = document.getElementById('selectionMode');
|
||||
if (selectedCards.size > 0) {
|
||||
selectionMode = true;
|
||||
selectionModeEl.style.display = 'flex';
|
||||
selectionModeEl.querySelector('.selection-count').textContent = `${selectedCards.size} selected`;
|
||||
} else {
|
||||
selectionMode = false;
|
||||
selectionModeEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('selectAll').addEventListener('change', (e) => {
|
||||
const cards = document.querySelectorAll('.card[data-index]');
|
||||
if (e.target.checked) {
|
||||
cards.forEach(card => {
|
||||
selectedCards.add(card.dataset.index);
|
||||
card.classList.add('selected');
|
||||
});
|
||||
} else {
|
||||
selectedCards.clear();
|
||||
cards.forEach(card => card.classList.remove('selected'));
|
||||
}
|
||||
updateSelectionMode();
|
||||
});
|
||||
|
||||
// Touch gestures for mobile
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let currentCard = null;
|
||||
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
currentCard = card;
|
||||
}, { passive: true });
|
||||
|
||||
card.addEventListener('touchmove', (e) => {
|
||||
if (!touchStartX || !currentCard) return;
|
||||
|
||||
const touchEndX = e.touches[0].clientX;
|
||||
const touchEndY = e.touches[0].clientY;
|
||||
const diffX = touchStartX - touchEndX;
|
||||
const diffY = touchStartY - touchEndY;
|
||||
|
||||
// Only handle horizontal swipes
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
||||
currentCard.classList.add('swiping');
|
||||
currentCard.style.transform = `translateX(${-diffX * 0.5}px)`;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
card.addEventListener('touchend', () => {
|
||||
if (currentCard) {
|
||||
currentCard.classList.remove('swiping');
|
||||
currentCard.style.transform = '';
|
||||
currentCard = null;
|
||||
}
|
||||
touchStartX = 0;
|
||||
touchStartY = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Flip card functionality
|
||||
document.querySelector('.flip-trigger').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
document.getElementById('flipCard').classList.add('flipped');
|
||||
});
|
||||
|
||||
// Expandable card functionality
|
||||
document.querySelector('.expand-trigger').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = document.getElementById('expandableCard');
|
||||
const isExpanded = card.classList.contains('expanded');
|
||||
card.classList.toggle('expanded');
|
||||
|
||||
const trigger = e.currentTarget;
|
||||
const text = trigger.querySelector('span');
|
||||
text.textContent = isExpanded ? 'Show more' : 'Show less';
|
||||
|
||||
// Announce state change for screen readers
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.className = 'sr-only';
|
||||
announcement.textContent = isExpanded ? 'Content collapsed' : 'Content expanded';
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => announcement.remove(), 1000);
|
||||
});
|
||||
|
||||
// Respect reduced motion preference
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
document.documentElement.style.setProperty('--transition', 'none');
|
||||
document.documentElement.style.setProperty('--transition-fast', 'none');
|
||||
}
|
||||
|
||||
// Screen reader only content
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }';
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,972 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>File Upload Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--primary-light: #e0e7ff;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--gray-800);
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
background: var(--gray-50);
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
border: 2px dashed var(--gray-300);
|
||||
border-radius: 12px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.upload-zone.drag-over {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.upload-zone.drag-over .upload-icon {
|
||||
animation: bounce 0.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: var(--gray-100);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.upload-icon svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
stroke: var(--gray-500);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.upload-zone.drag-over .upload-icon {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.upload-zone.drag-over .upload-icon svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.upload-text h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.upload-text p {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-button {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-button:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.file-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.supported-formats {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
background: var(--gray-100);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-preview.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
stroke: var(--gray-500);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.status-uploading {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--gray-100);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
.action-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: var(--gray-600);
|
||||
}
|
||||
|
||||
.action-button.danger:hover {
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.action-button.danger:hover svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--gray-200);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary) 0%, #8b5cf6 100%);
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
|
||||
animation: shimmer 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.paste-hint {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: var(--gray-800);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.paste-hint.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.upload-stats {
|
||||
margin-top: 32px;
|
||||
padding: 20px;
|
||||
background: var(--gray-50);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: var(--error);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.upload-stats {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.file-input:focus + .file-button {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>File Upload - Enhanced</h1>
|
||||
<p class="subtitle">Drag & drop files or click to browse. Supports images, documents, and more.</p>
|
||||
|
||||
<div class="upload-container">
|
||||
<div class="upload-zone" id="uploadZone">
|
||||
<div class="upload-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<h3>Drop files here or click to upload</h3>
|
||||
<p>You can also paste images from your clipboard</p>
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" class="file-input" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.txt,.zip">
|
||||
<button class="file-button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
Choose Files
|
||||
</button>
|
||||
</div>
|
||||
<p class="supported-formats">Supported: Images, PDF, DOC, TXT, ZIP (Max 10MB per file)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paste-hint" id="pasteHint">Image copied! Press Ctrl+V to upload</div>
|
||||
|
||||
<div class="file-list" id="fileList"></div>
|
||||
|
||||
<div class="upload-stats" id="uploadStats" style="display: none;">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="totalFiles">0</div>
|
||||
<div class="stat-label">Total Files</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="uploadedFiles">0</div>
|
||||
<div class="stat-label">Uploaded</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="totalSize">0 MB</div>
|
||||
<div class="stat-label">Total Size</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
class FileUploadEnhanced {
|
||||
constructor() {
|
||||
this.uploadZone = document.getElementById('uploadZone');
|
||||
this.fileInput = document.getElementById('fileInput');
|
||||
this.fileList = document.getElementById('fileList');
|
||||
this.pasteHint = document.getElementById('pasteHint');
|
||||
this.uploadStats = document.getElementById('uploadStats');
|
||||
|
||||
this.files = new Map();
|
||||
this.uploadQueue = [];
|
||||
this.isUploading = false;
|
||||
this.maxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
this.chunkSize = 1024 * 1024; // 1MB chunks
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// File input
|
||||
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||||
|
||||
// Drag and drop
|
||||
this.uploadZone.addEventListener('dragover', (e) => this.handleDragOver(e));
|
||||
this.uploadZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||||
this.uploadZone.addEventListener('drop', (e) => this.handleDrop(e));
|
||||
|
||||
// Paste from clipboard
|
||||
document.addEventListener('paste', (e) => this.handlePaste(e));
|
||||
|
||||
// Copy event detection
|
||||
document.addEventListener('copy', () => this.showPasteHint());
|
||||
|
||||
// Prevent default drag behavior
|
||||
document.addEventListener('dragover', (e) => e.preventDefault());
|
||||
document.addEventListener('drop', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
handleFileSelect(e) {
|
||||
const files = Array.from(e.target.files);
|
||||
this.processFiles(files);
|
||||
e.target.value = ''; // Reset input
|
||||
}
|
||||
|
||||
handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.uploadZone.classList.add('drag-over');
|
||||
}
|
||||
|
||||
handleDragLeave(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.uploadZone.contains(e.relatedTarget)) {
|
||||
this.uploadZone.classList.remove('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.uploadZone.classList.remove('drag-over');
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
this.processFiles(files);
|
||||
}
|
||||
|
||||
handlePaste(e) {
|
||||
const items = Array.from(e.clipboardData.items);
|
||||
const files = [];
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
// Give pasted images a name
|
||||
const renamedFile = new File([file], `pasted-image-${Date.now()}.png`, {
|
||||
type: file.type
|
||||
});
|
||||
files.push(renamedFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (files.length > 0) {
|
||||
this.processFiles(files);
|
||||
this.hidePasteHint();
|
||||
}
|
||||
}
|
||||
|
||||
showPasteHint() {
|
||||
this.pasteHint.classList.add('show');
|
||||
setTimeout(() => this.hidePasteHint(), 3000);
|
||||
}
|
||||
|
||||
hidePasteHint() {
|
||||
this.pasteHint.classList.remove('show');
|
||||
}
|
||||
|
||||
processFiles(files) {
|
||||
files.forEach(file => {
|
||||
const validation = this.validateFile(file);
|
||||
if (validation.valid) {
|
||||
this.addFile(file);
|
||||
} else {
|
||||
this.showError(validation.error);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.processUploadQueue();
|
||||
}
|
||||
|
||||
validateFile(file) {
|
||||
// Check file size
|
||||
if (file.size > this.maxFileSize) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${file.name} exceeds the 10MB size limit`
|
||||
};
|
||||
}
|
||||
|
||||
// Check file type
|
||||
const allowedTypes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain', 'application/zip'
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(file.type) && !file.type.startsWith('image/')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${file.name} is not a supported file type`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
addFile(file) {
|
||||
const fileId = this.generateId();
|
||||
const fileData = {
|
||||
id: fileId,
|
||||
file: file,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
uploaded: 0,
|
||||
element: null
|
||||
};
|
||||
|
||||
this.files.set(fileId, fileData);
|
||||
this.uploadQueue.push(fileId);
|
||||
|
||||
const element = this.createFileElement(fileData);
|
||||
fileData.element = element;
|
||||
this.fileList.appendChild(element);
|
||||
|
||||
if (this.uploadStats.style.display === 'none') {
|
||||
this.uploadStats.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Load preview for images
|
||||
if (file.type.startsWith('image/')) {
|
||||
this.loadImagePreview(fileData);
|
||||
}
|
||||
}
|
||||
|
||||
createFileElement(fileData) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'file-item';
|
||||
div.id = `file-${fileData.id}`;
|
||||
|
||||
const isImage = fileData.file.type.startsWith('image/');
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="file-preview ${isImage ? 'loading' : ''}">
|
||||
${this.getFileIcon(fileData.file)}
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${fileData.file.name}</div>
|
||||
<div class="file-meta">
|
||||
<div class="file-size">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
${this.formatFileSize(fileData.file.size)}
|
||||
</div>
|
||||
<div class="file-status status-uploading">
|
||||
<svg class="status-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span>Waiting...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="action-button" onclick="fileUpload.retryUpload('${fileData.id}')" title="Retry" style="display: none;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-button danger" onclick="fileUpload.removeFile('${fileData.id}')" title="Remove">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
getFileIcon(file) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
return `<img src="" alt="${file.name}" style="display: none;">`;
|
||||
}
|
||||
|
||||
let icon;
|
||||
if (file.type === 'application/pdf') {
|
||||
icon = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline>';
|
||||
} else if (file.type.includes('zip')) {
|
||||
icon = '<path d="M22 11v1a10 10 0 1 1-9-10"></path><polyline points="22 2 12 12.01 8 8"></polyline>';
|
||||
} else {
|
||||
icon = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline>';
|
||||
}
|
||||
|
||||
return `<svg class="file-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${icon}</svg>`;
|
||||
}
|
||||
|
||||
loadImagePreview(fileData) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const preview = fileData.element.querySelector('.file-preview');
|
||||
const img = preview.querySelector('img');
|
||||
img.src = e.target.result;
|
||||
img.style.display = 'block';
|
||||
preview.classList.remove('loading');
|
||||
};
|
||||
reader.readAsDataURL(fileData.file);
|
||||
}
|
||||
|
||||
async processUploadQueue() {
|
||||
if (this.isUploading || this.uploadQueue.length === 0) return;
|
||||
|
||||
this.isUploading = true;
|
||||
const fileId = this.uploadQueue.shift();
|
||||
const fileData = this.files.get(fileId);
|
||||
|
||||
if (fileData && fileData.status === 'pending') {
|
||||
await this.uploadFile(fileData);
|
||||
}
|
||||
|
||||
this.isUploading = false;
|
||||
this.processUploadQueue();
|
||||
}
|
||||
|
||||
async uploadFile(fileData) {
|
||||
fileData.status = 'uploading';
|
||||
this.updateFileStatus(fileData, 'Uploading...', 'status-uploading');
|
||||
|
||||
try {
|
||||
// Simulate chunked upload
|
||||
const chunks = Math.ceil(fileData.file.size / this.chunkSize);
|
||||
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
// Simulate upload delay
|
||||
await this.delay(300 + Math.random() * 200);
|
||||
|
||||
// Update progress
|
||||
fileData.progress = Math.min(((i + 1) / chunks) * 100, 100);
|
||||
this.updateProgress(fileData);
|
||||
|
||||
// Simulate random failure (5% chance)
|
||||
if (Math.random() < 0.05 && i > 0) {
|
||||
throw new Error('Network error');
|
||||
}
|
||||
}
|
||||
|
||||
// Upload complete
|
||||
fileData.status = 'completed';
|
||||
fileData.uploaded = fileData.file.size;
|
||||
this.updateFileStatus(fileData, 'Uploaded', 'status-success');
|
||||
this.showSuccess(fileData);
|
||||
|
||||
} catch (error) {
|
||||
fileData.status = 'error';
|
||||
this.updateFileStatus(fileData, 'Failed', 'status-error');
|
||||
this.showRetryButton(fileData);
|
||||
}
|
||||
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
updateFileStatus(fileData, text, className) {
|
||||
const statusElement = fileData.element.querySelector('.file-status');
|
||||
statusElement.className = `file-status ${className}`;
|
||||
statusElement.querySelector('span').textContent = text;
|
||||
}
|
||||
|
||||
updateProgress(fileData) {
|
||||
const progressFill = fileData.element.querySelector('.progress-fill');
|
||||
progressFill.style.width = `${fileData.progress}%`;
|
||||
}
|
||||
|
||||
showSuccess(fileData) {
|
||||
const element = fileData.element;
|
||||
element.style.animation = 'none';
|
||||
element.offsetHeight; // Trigger reflow
|
||||
element.style.animation = 'slideIn 0.3s ease';
|
||||
|
||||
// Hide progress bar after animation
|
||||
setTimeout(() => {
|
||||
const progressBar = element.querySelector('.progress-bar');
|
||||
progressBar.style.opacity = '0';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
showRetryButton(fileData) {
|
||||
const retryButton = fileData.element.querySelector('.action-button');
|
||||
retryButton.style.display = 'flex';
|
||||
}
|
||||
|
||||
retryUpload(fileId) {
|
||||
const fileData = this.files.get(fileId);
|
||||
if (fileData) {
|
||||
fileData.status = 'pending';
|
||||
fileData.progress = 0;
|
||||
this.updateProgress(fileData);
|
||||
this.updateFileStatus(fileData, 'Waiting...', 'status-uploading');
|
||||
|
||||
const retryButton = fileData.element.querySelector('.action-button');
|
||||
retryButton.style.display = 'none';
|
||||
|
||||
const progressBar = fileData.element.querySelector('.progress-bar');
|
||||
progressBar.style.opacity = '1';
|
||||
|
||||
this.uploadQueue.push(fileId);
|
||||
this.processUploadQueue();
|
||||
}
|
||||
}
|
||||
|
||||
removeFile(fileId) {
|
||||
const fileData = this.files.get(fileId);
|
||||
if (fileData) {
|
||||
// Cancel upload if in progress
|
||||
if (fileData.status === 'uploading') {
|
||||
const index = this.uploadQueue.indexOf(fileId);
|
||||
if (index > -1) {
|
||||
this.uploadQueue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove with animation
|
||||
fileData.element.style.transform = 'translateX(100%)';
|
||||
fileData.element.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
fileData.element.remove();
|
||||
this.files.delete(fileId);
|
||||
this.updateStats();
|
||||
|
||||
if (this.files.size === 0) {
|
||||
this.uploadStats.style.display = 'none';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
let totalSize = 0;
|
||||
let uploadedCount = 0;
|
||||
|
||||
this.files.forEach(fileData => {
|
||||
totalSize += fileData.file.size;
|
||||
if (fileData.status === 'completed') {
|
||||
uploadedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('totalFiles').textContent = this.files.size;
|
||||
document.getElementById('uploadedFiles').textContent = uploadedCount;
|
||||
document.getElementById('totalSize').textContent = this.formatFileSize(totalSize);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const error = document.createElement('div');
|
||||
error.className = 'error-message';
|
||||
error.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
${message}
|
||||
`;
|
||||
|
||||
this.uploadZone.appendChild(error);
|
||||
|
||||
setTimeout(() => {
|
||||
error.style.opacity = '0';
|
||||
setTimeout(() => error.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return 'file-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the enhanced file upload
|
||||
const fileUpload = new FileUploadEnhanced();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,988 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toast Notifications Enhanced</title>
|
||||
<style>
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
main {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
text-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Demo controls */
|
||||
.controls {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-group h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.demo-button:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.demo-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.demo-button.success {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.demo-button.error {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
|
||||
.demo-button.warning {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.demo-button.info {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
}
|
||||
|
||||
/* Position selector */
|
||||
.position-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.position-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.position-button:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.position-button.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* Toast container positions */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-container.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.toast-container.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toast-container.top-center {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-container.bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
align-items: flex-end;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.toast-container.bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
align-items: flex-start;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.toast-container.bottom-center {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* Toast styles */
|
||||
.toast {
|
||||
pointer-events: auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
padding: 1rem 1.5rem;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
animation: slideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container.top-left .toast,
|
||||
.toast-container.bottom-left .toast {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container.top-center .toast,
|
||||
.toast-container.bottom-center .toast {
|
||||
animation-name: slideInTop;
|
||||
}
|
||||
|
||||
@keyframes slideInTop {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: slideOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
to {
|
||||
transform: translateX(110%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container.top-left .toast.removing,
|
||||
.toast-container.bottom-left .toast.removing {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
to {
|
||||
transform: translateX(-110%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 15px 50px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Toast icon */
|
||||
.toast-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toast-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toast.success .toast-icon {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.toast.error .toast-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.toast.warning .toast-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast.info .toast-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Toast content */
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.875rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Toast actions */
|
||||
.toast-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.toast-action {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.toast-action:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.toast-action.primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-action.primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
.toast-close {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #a0aec0;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: currentColor;
|
||||
opacity: 0.2;
|
||||
transition: width linear;
|
||||
}
|
||||
|
||||
.toast.success .toast-progress {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.toast.error .toast-progress {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.toast.warning .toast-progress {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast.info .toast-progress {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 640px) {
|
||||
.toast {
|
||||
min-width: calc(100vw - 2rem);
|
||||
max-width: calc(100vw - 2rem);
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility focus styles */
|
||||
.toast:focus-visible,
|
||||
.toast-action:focus-visible,
|
||||
.toast-close:focus-visible {
|
||||
outline: 2px solid #667eea;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.toast {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to { opacity: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Stats display */
|
||||
.stats {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 1rem;
|
||||
background: #f7fafc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Toast Notifications - Enhanced</h1>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>Toast Position</h3>
|
||||
<div class="position-selector">
|
||||
<button class="position-button active" data-position="top-right">Top Right</button>
|
||||
<button class="position-button" data-position="top-center">Top Center</button>
|
||||
<button class="position-button" data-position="top-left">Top Left</button>
|
||||
<button class="position-button" data-position="bottom-right">Bottom Right</button>
|
||||
<button class="position-button" data-position="bottom-center">Bottom Center</button>
|
||||
<button class="position-button" data-position="bottom-left">Bottom Left</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>Create Toasts</h3>
|
||||
<div class="button-grid">
|
||||
<button class="demo-button success" data-type="success">Success Toast</button>
|
||||
<button class="demo-button error" data-type="error">Error Toast</button>
|
||||
<button class="demo-button warning" data-type="warning">Warning Toast</button>
|
||||
<button class="demo-button info" data-type="info">Info Toast</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>Special Toasts</h3>
|
||||
<div class="button-grid">
|
||||
<button class="demo-button info" id="actionToast">Toast with Actions</button>
|
||||
<button class="demo-button warning" id="undoToast">Undo Toast</button>
|
||||
<button class="demo-button success" id="multiToast">Multiple Toasts</button>
|
||||
<button class="demo-button error" id="persistentToast">Persistent Toast</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="activeCount">0</div>
|
||||
<div class="stat-label">Active Toasts</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="queueCount">0</div>
|
||||
<div class="stat-label">Queued</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="totalCount">0</div>
|
||||
<div class="stat-label">Total Shown</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="dismissedCount">0</div>
|
||||
<div class="stat-label">Dismissed</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast containers will be created dynamically -->
|
||||
|
||||
<script>
|
||||
// Toast notification system
|
||||
class ToastSystem {
|
||||
constructor() {
|
||||
this.toasts = new Map();
|
||||
this.queue = [];
|
||||
this.position = 'top-right';
|
||||
this.maxVisible = 5;
|
||||
this.defaultDuration = 5000;
|
||||
this.stats = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
queued: 0,
|
||||
dismissed: 0
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createContainers();
|
||||
this.setupEventListeners();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
createContainers() {
|
||||
const positions = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'];
|
||||
positions.forEach(position => {
|
||||
const container = document.createElement('div');
|
||||
container.className = `toast-container ${position}`;
|
||||
container.style.display = position === this.position ? 'flex' : 'none';
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Position buttons
|
||||
document.querySelectorAll('.position-button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
this.setPosition(button.dataset.position);
|
||||
document.querySelectorAll('.position-button').forEach(b => b.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Toast type buttons
|
||||
document.querySelectorAll('.demo-button[data-type]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.dataset.type;
|
||||
this.showToast({
|
||||
type,
|
||||
title: this.getTitle(type),
|
||||
message: this.getMessage(type)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Special toasts
|
||||
document.getElementById('actionToast').addEventListener('click', () => {
|
||||
this.showToast({
|
||||
type: 'info',
|
||||
title: 'New Message',
|
||||
message: 'You have a new message from Sarah',
|
||||
actions: [
|
||||
{ text: 'View', primary: true, callback: () => console.log('View clicked') },
|
||||
{ text: 'Dismiss', callback: () => {} }
|
||||
],
|
||||
duration: 0 // No auto-dismiss
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('undoToast').addEventListener('click', () => {
|
||||
this.showToast({
|
||||
type: 'warning',
|
||||
title: 'Item Deleted',
|
||||
message: 'The item has been moved to trash',
|
||||
actions: [
|
||||
{ text: 'Undo', primary: true, callback: () => {
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: 'Restored',
|
||||
message: 'Item has been restored'
|
||||
});
|
||||
}}
|
||||
],
|
||||
duration: 10000
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('multiToast').addEventListener('click', () => {
|
||||
const messages = [
|
||||
'First notification',
|
||||
'Second notification',
|
||||
'Third notification',
|
||||
'Fourth notification',
|
||||
'Fifth notification',
|
||||
'Sixth notification (queued)',
|
||||
'Seventh notification (queued)'
|
||||
];
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
setTimeout(() => {
|
||||
this.showToast({
|
||||
type: ['success', 'info', 'warning'][index % 3],
|
||||
title: `Notification ${index + 1}`,
|
||||
message
|
||||
});
|
||||
}, index * 200);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('persistentToast').addEventListener('click', () => {
|
||||
this.showToast({
|
||||
type: 'error',
|
||||
title: 'Connection Lost',
|
||||
message: 'Please check your internet connection',
|
||||
duration: 0, // No auto-dismiss
|
||||
closable: false // Can't be closed manually
|
||||
});
|
||||
|
||||
// Simulate reconnection after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: 'Connected',
|
||||
message: 'Connection restored'
|
||||
});
|
||||
// Find and remove the persistent toast
|
||||
this.toasts.forEach((toast, id) => {
|
||||
if (toast.config.title === 'Connection Lost') {
|
||||
this.removeToast(id);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
// Dismiss all toasts
|
||||
this.toasts.forEach((toast, id) => {
|
||||
if (toast.config.closable !== false) {
|
||||
this.removeToast(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setPosition(position) {
|
||||
this.position = position;
|
||||
document.querySelectorAll('.toast-container').forEach(container => {
|
||||
container.style.display = container.classList.contains(position) ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
// Move existing toasts to new container
|
||||
const newContainer = document.querySelector(`.toast-container.${position}`);
|
||||
this.toasts.forEach(toast => {
|
||||
newContainer.appendChild(toast.element);
|
||||
});
|
||||
}
|
||||
|
||||
showToast(config) {
|
||||
const id = Date.now() + Math.random();
|
||||
const toast = {
|
||||
id,
|
||||
config: {
|
||||
duration: this.defaultDuration,
|
||||
closable: true,
|
||||
...config
|
||||
},
|
||||
element: null,
|
||||
progressInterval: null
|
||||
};
|
||||
|
||||
// Check if we need to queue this toast
|
||||
if (this.toasts.size >= this.maxVisible) {
|
||||
this.queue.push(toast);
|
||||
this.stats.queued++;
|
||||
this.updateStats();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createToastElement(toast);
|
||||
this.toasts.set(id, toast);
|
||||
this.stats.total++;
|
||||
this.stats.active++;
|
||||
this.updateStats();
|
||||
|
||||
// Auto-dismiss if duration is set
|
||||
if (toast.config.duration > 0) {
|
||||
this.startProgress(toast);
|
||||
}
|
||||
}
|
||||
|
||||
createToastElement(toast) {
|
||||
const element = document.createElement('div');
|
||||
element.className = `toast ${toast.config.type}`;
|
||||
element.setAttribute('role', 'alert');
|
||||
element.setAttribute('aria-live', 'polite');
|
||||
element.tabIndex = 0;
|
||||
|
||||
// Icon
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'toast-icon';
|
||||
icon.innerHTML = this.getIcon(toast.config.type);
|
||||
element.appendChild(icon);
|
||||
|
||||
// Content
|
||||
const content = document.createElement('div');
|
||||
content.className = 'toast-content';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'toast-title';
|
||||
title.textContent = toast.config.title;
|
||||
content.appendChild(title);
|
||||
|
||||
const message = document.createElement('div');
|
||||
message.className = 'toast-message';
|
||||
message.textContent = toast.config.message;
|
||||
content.appendChild(message);
|
||||
|
||||
element.appendChild(content);
|
||||
|
||||
// Actions
|
||||
if (toast.config.actions && toast.config.actions.length > 0) {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'toast-actions';
|
||||
|
||||
toast.config.actions.forEach(action => {
|
||||
const button = document.createElement('button');
|
||||
button.className = `toast-action ${action.primary ? 'primary' : ''}`;
|
||||
button.textContent = action.text;
|
||||
button.addEventListener('click', () => {
|
||||
action.callback();
|
||||
if (action.dismiss !== false) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
});
|
||||
actions.appendChild(button);
|
||||
});
|
||||
|
||||
element.appendChild(actions);
|
||||
}
|
||||
|
||||
// Close button
|
||||
if (toast.config.closable !== false) {
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.className = 'toast-close';
|
||||
closeButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
||||
closeButton.setAttribute('aria-label', 'Close notification');
|
||||
closeButton.addEventListener('click', () => this.removeToast(toast.id));
|
||||
element.appendChild(closeButton);
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
if (toast.config.duration > 0) {
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'toast-progress';
|
||||
progress.style.width = '100%';
|
||||
element.appendChild(progress);
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
element.addEventListener('mouseenter', () => this.pauseProgress(toast));
|
||||
element.addEventListener('mouseleave', () => this.resumeProgress(toast));
|
||||
|
||||
// Touch swipe to dismiss
|
||||
let touchStartX = 0;
|
||||
element.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
});
|
||||
|
||||
element.addEventListener('touchend', (e) => {
|
||||
const touchEndX = e.changedTouches[0].clientX;
|
||||
const diff = touchEndX - touchStartX;
|
||||
if (Math.abs(diff) > 50 && toast.config.closable !== false) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Click to dismiss (if no actions)
|
||||
if (!toast.config.actions || toast.config.actions.length === 0) {
|
||||
element.style.cursor = 'pointer';
|
||||
element.addEventListener('click', () => {
|
||||
if (toast.config.closable !== false) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toast.element = element;
|
||||
|
||||
// Add to container
|
||||
const container = document.querySelector(`.toast-container.${this.position}`);
|
||||
container.appendChild(element);
|
||||
|
||||
// Focus for accessibility
|
||||
element.focus();
|
||||
}
|
||||
|
||||
startProgress(toast) {
|
||||
if (!toast.config.duration || toast.progressInterval) return;
|
||||
|
||||
const progress = toast.element.querySelector('.toast-progress');
|
||||
if (!progress) return;
|
||||
|
||||
const startTime = Date.now();
|
||||
const duration = toast.config.duration;
|
||||
|
||||
toast.progressInterval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const percentage = Math.max(0, 100 - (elapsed / duration) * 100);
|
||||
progress.style.width = `${percentage}%`;
|
||||
progress.style.transition = 'width 100ms linear';
|
||||
|
||||
if (percentage <= 0) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
pauseProgress(toast) {
|
||||
if (toast.progressInterval) {
|
||||
clearInterval(toast.progressInterval);
|
||||
toast.progressInterval = null;
|
||||
const progress = toast.element.querySelector('.toast-progress');
|
||||
if (progress) {
|
||||
toast.pausedWidth = progress.style.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumeProgress(toast) {
|
||||
if (!toast.config.duration || toast.progressInterval) return;
|
||||
|
||||
const progress = toast.element.querySelector('.toast-progress');
|
||||
if (!progress || !toast.pausedWidth) return;
|
||||
|
||||
const remainingPercentage = parseFloat(toast.pausedWidth);
|
||||
const remainingDuration = (remainingPercentage / 100) * toast.config.duration;
|
||||
|
||||
const startTime = Date.now();
|
||||
toast.progressInterval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const percentage = Math.max(0, remainingPercentage - (elapsed / remainingDuration) * remainingPercentage);
|
||||
progress.style.width = `${percentage}%`;
|
||||
|
||||
if (percentage <= 0) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
removeToast(id) {
|
||||
const toast = this.toasts.get(id);
|
||||
if (!toast) return;
|
||||
|
||||
// Clear progress interval
|
||||
if (toast.progressInterval) {
|
||||
clearInterval(toast.progressInterval);
|
||||
}
|
||||
|
||||
// Animate out
|
||||
toast.element.classList.add('removing');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.element.remove();
|
||||
this.toasts.delete(id);
|
||||
this.stats.active--;
|
||||
this.stats.dismissed++;
|
||||
this.updateStats();
|
||||
|
||||
// Process queue
|
||||
if (this.queue.length > 0 && this.toasts.size < this.maxVisible) {
|
||||
const nextToast = this.queue.shift();
|
||||
this.stats.queued--;
|
||||
this.createToastElement(nextToast);
|
||||
this.toasts.set(nextToast.id, nextToast);
|
||||
this.stats.active++;
|
||||
this.updateStats();
|
||||
|
||||
if (nextToast.config.duration > 0) {
|
||||
this.startProgress(nextToast);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
document.getElementById('activeCount').textContent = this.stats.active;
|
||||
document.getElementById('queueCount').textContent = this.stats.queued;
|
||||
document.getElementById('totalCount').textContent = this.stats.total;
|
||||
document.getElementById('dismissedCount').textContent = this.stats.dismissed;
|
||||
}
|
||||
|
||||
getIcon(type) {
|
||||
const icons = {
|
||||
success: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
||||
error: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
||||
warning: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>',
|
||||
info: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
getTitle(type) {
|
||||
const titles = {
|
||||
success: 'Success!',
|
||||
error: 'Error!',
|
||||
warning: 'Warning!',
|
||||
info: 'Information'
|
||||
};
|
||||
return titles[type] || 'Notification';
|
||||
}
|
||||
|
||||
getMessage(type) {
|
||||
const messages = {
|
||||
success: 'Your action was completed successfully.',
|
||||
error: 'Something went wrong. Please try again.',
|
||||
warning: 'Please review this important information.',
|
||||
info: 'Here\'s something you might want to know.'
|
||||
};
|
||||
return messages[type] || 'This is a notification message.';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the toast system
|
||||
const toastSystem = new ToastSystem();
|
||||
|
||||
// Show a welcome toast after page load
|
||||
setTimeout(() => {
|
||||
toastSystem.showToast({
|
||||
type: 'info',
|
||||
title: 'Welcome!',
|
||||
message: 'Try the different toast types and positions.',
|
||||
duration: 7000
|
||||
});
|
||||
}, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue