progress
This commit is contained in:
parent
ef1a41d365
commit
189d92a771
|
|
@ -0,0 +1,360 @@
|
|||
# Themed Hybrid UI Component Specification v4
|
||||
|
||||
## Evolution from v3: Modular Architecture
|
||||
|
||||
This specification builds upon v3's successful themed hybrid component approach with a critical architectural improvement: **separation of concerns through modular file structure**. While v3 delivered powerful themed components in single HTML files, v4 embraces modern development practices by splitting each component into three distinct files within organized directories.
|
||||
|
||||
### Key Improvements in v4:
|
||||
- **Maintainability**: Styles and scripts can be modified without touching HTML structure
|
||||
- **Reusability**: CSS themes and JavaScript behaviors can be extended or shared
|
||||
- **Performance**: Better browser caching, conditional loading, and optimization opportunities
|
||||
- **Collaboration**: Teams can work on styling, structure, and behavior independently
|
||||
- **Scalability**: Components are ready for integration into larger systems
|
||||
- **Developer Experience**: Clean separation follows industry best practices
|
||||
|
||||
## Core Challenge (Enhanced)
|
||||
Create a **uniquely themed UI component** that combines multiple existing UI elements into one elegant solution, now with **professional-grade file organization** that demonstrates mastery of modern web development practices.
|
||||
|
||||
Apply a distinctive design language while solving multiple interface problems in a single, cohesive component - achieving "two birds with one stone" efficiency through both functional integration and architectural excellence.
|
||||
|
||||
## Output Requirements
|
||||
|
||||
**Directory Structure**: `ui_hybrid_[iteration_number]/`
|
||||
|
||||
Each iteration creates its own directory containing exactly three files:
|
||||
```
|
||||
ui_hybrid_[iteration_number]/
|
||||
├── index.html # Semantic HTML structure
|
||||
├── styles.css # Complete styling and theme implementation
|
||||
└── script.js # All JavaScript functionality and interactions
|
||||
```
|
||||
|
||||
**File Naming**:
|
||||
- Directory: `ui_hybrid_[iteration_number]` (e.g., `ui_hybrid_1`, `ui_hybrid_2`)
|
||||
- Files: Always `index.html`, `styles.css`, `script.js` (consistent naming)
|
||||
|
||||
## Content Structure
|
||||
|
||||
### **index.html** - Semantic Structure
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>[Theme Name] [Hybrid Component Name]</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>[Hybrid Component Name] - [Theme Name] Theme</h1>
|
||||
|
||||
<!-- Clean semantic HTML structure -->
|
||||
<!-- No inline styles or scripts -->
|
||||
<div class="hybrid-component">
|
||||
<!-- Component structure with meaningful class names -->
|
||||
<!-- Data attributes for JavaScript hooks -->
|
||||
<!-- Accessibility attributes (ARIA labels, roles) -->
|
||||
</div>
|
||||
|
||||
<!-- Additional component instances or examples -->
|
||||
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### **styles.css** - Complete Theme Implementation
|
||||
```css
|
||||
/* Theme Variables and Custom Properties */
|
||||
:root {
|
||||
/* Color palette for the theme */
|
||||
/* Typography scale */
|
||||
/* Animation timings */
|
||||
/* Spacing system */
|
||||
}
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Theme Foundation */
|
||||
body {
|
||||
/* Theme-specific base styles */
|
||||
/* Background treatments */
|
||||
/* Default typography */
|
||||
}
|
||||
|
||||
/* Component Architecture */
|
||||
.hybrid-component {
|
||||
/* Main component container */
|
||||
/* Theme-specific treatments */
|
||||
}
|
||||
|
||||
/* Component Sub-elements */
|
||||
.hybrid-component__[element] {
|
||||
/* BEM or consistent naming convention */
|
||||
/* Element-specific theme styling */
|
||||
}
|
||||
|
||||
/* State Classes */
|
||||
.is-active, .is-loading, .is-error {
|
||||
/* State-based styling */
|
||||
/* Theme-consistent feedback */
|
||||
}
|
||||
|
||||
/* Animations and Transitions */
|
||||
@keyframes [themeAnimation] {
|
||||
/* Theme-specific animations */
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
/* Mobile adaptations maintaining theme */
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
/* Print-optimized theme variant */
|
||||
}
|
||||
```
|
||||
|
||||
### **script.js** - Functionality and Interactions
|
||||
```javascript
|
||||
// Strict mode for better error catching
|
||||
'use strict';
|
||||
|
||||
// Theme Configuration
|
||||
const THEME_CONFIG = {
|
||||
// Animation durations
|
||||
// API endpoints if needed
|
||||
// Theme-specific settings
|
||||
};
|
||||
|
||||
// Component State Management
|
||||
class HybridComponent {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.state = {
|
||||
// Component state properties
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Setup event listeners
|
||||
// Initialize sub-components
|
||||
// Load any necessary data
|
||||
this.bindEvents();
|
||||
this.setupThemeFeatures();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Event delegation for efficiency
|
||||
// Touch and mouse events
|
||||
// Keyboard navigation
|
||||
}
|
||||
|
||||
setupThemeFeatures() {
|
||||
// Theme-specific interactions
|
||||
// Special effects or behaviors
|
||||
// Animation triggers
|
||||
}
|
||||
|
||||
// Component Methods
|
||||
updateState(updates) {
|
||||
// State management logic
|
||||
// UI updates based on state
|
||||
}
|
||||
|
||||
// API Methods if needed
|
||||
async fetchData() {
|
||||
// Data loading with error handling
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
debounce(func, wait) {
|
||||
// Performance optimizations
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on DOM Ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Find all component instances
|
||||
const components = document.querySelectorAll('.hybrid-component');
|
||||
|
||||
// Initialize each instance
|
||||
components.forEach(element => {
|
||||
new HybridComponent(element);
|
||||
});
|
||||
|
||||
// Setup any global theme features
|
||||
initializeThemeEffects();
|
||||
});
|
||||
|
||||
// Global Theme Functions
|
||||
function initializeThemeEffects() {
|
||||
// Ambient animations
|
||||
// Parallax effects
|
||||
// Theme-wide interactions
|
||||
}
|
||||
|
||||
// Export for potential module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HybridComponent;
|
||||
}
|
||||
```
|
||||
|
||||
## Design Dimensions (Preserved from v3)
|
||||
|
||||
### **Unique Theme Development**
|
||||
Each component must embody a distinctive design language that creates personality and memorable experience. The multi-file structure enhances theme implementation:
|
||||
|
||||
#### **Enhanced Theme Implementation**
|
||||
- **CSS Variables**: Define theme tokens in `:root` for consistent application
|
||||
- **Modular Styles**: Theme variations can be swapped by changing stylesheets
|
||||
- **JavaScript Theming**: Dynamic theme features separated from core functionality
|
||||
- **Asset Organization**: Theme-specific assets referenced properly from each file
|
||||
|
||||
[All theme categories from v3 remain the same: Organic Nature, Digital Minimalism, Retro Computing, etc.]
|
||||
|
||||
### **Hybrid Component Strategy**
|
||||
The same powerful combinations from v3, now with better architectural separation:
|
||||
|
||||
#### **Architectural Benefits per Component Type**
|
||||
- **Search Hub**: Search logic isolated in JS, theme animations in CSS
|
||||
- **Input Intelligence**: Validation rules in JS, visual feedback in CSS
|
||||
- **Data Explorer**: Sorting algorithms in JS, table styling in CSS
|
||||
- **Media Player**: Playback logic in JS, visualizer styles in CSS
|
||||
|
||||
[All component combinations from v3 remain valid]
|
||||
|
||||
## Enhancement Principles (Evolved)
|
||||
|
||||
### **Architectural Excellence** (New in v4)
|
||||
- **Separation of Concerns**: Each file has a single, clear responsibility
|
||||
- **No Inline Styles/Scripts**: All styling in CSS, all behavior in JavaScript
|
||||
- **Progressive Enhancement**: HTML works without CSS/JS, enhanced by both
|
||||
- **Module Boundaries**: Clear interfaces between files, no tight coupling
|
||||
- **Future-Ready**: Structure supports build tools, frameworks, component libraries
|
||||
|
||||
### **Development Best Practices** (New in v4)
|
||||
- **CSS Organization**: Logical section ordering, consistent naming conventions
|
||||
- **JavaScript Patterns**: Modern ES6+, class-based or functional approaches
|
||||
- **HTML Semantics**: Proper element selection, accessibility-first markup
|
||||
- **Performance Focus**: Optimized selectors, efficient event handling
|
||||
- **Documentation**: Clear comments explaining theme decisions and component logic
|
||||
|
||||
[All original enhancement principles from v3 remain: Thematic Consistency, Functional Integration, Practical Excellence]
|
||||
|
||||
## File Integration Guidelines
|
||||
|
||||
### **Linking Strategy**
|
||||
- **Consistent Paths**: Always use relative paths (`href="styles.css"`)
|
||||
- **Load Order**: CSS in `<head>`, JavaScript before `</body>`
|
||||
- **No CDNs**: All functionality self-contained within the three files
|
||||
- **Fallbacks**: Graceful degradation if CSS or JS fails to load
|
||||
|
||||
### **Communication Between Files**
|
||||
- **HTML → CSS**: Semantic class names, data attributes for styling hooks
|
||||
- **HTML → JS**: IDs for unique elements, data attributes for configuration
|
||||
- **CSS → JS**: CSS custom properties readable by JavaScript
|
||||
- **JS → CSS**: Dynamic class additions, CSS variable updates
|
||||
|
||||
### **Naming Conventions**
|
||||
- **CSS Classes**: BEM, semantic, or consistent methodology
|
||||
- **JavaScript**: camelCase for variables/functions, PascalCase for classes
|
||||
- **Data Attributes**: `data-component-*` for component-specific data
|
||||
- **CSS Variables**: `--theme-*` prefix for theme variables
|
||||
|
||||
## Quality Standards (Enhanced)
|
||||
|
||||
### **Code Quality** (New in v4)
|
||||
- **Valid HTML**: Passes W3C validation, proper semantic structure
|
||||
- **CSS Organization**: Logical property grouping, no redundancy
|
||||
- **JavaScript Quality**: No global pollution, proper error handling
|
||||
- **Cross-Browser**: Works in all modern browsers (Chrome, Firefox, Safari, Edge)
|
||||
- **Performance**: Lighthouse score of 90+ in all categories
|
||||
|
||||
### **File-Specific Standards** (New in v4)
|
||||
- **HTML**: Semantic, accessible, minimal, no presentation logic
|
||||
- **CSS**: Organized, maintainable, efficient selectors, mobile-first
|
||||
- **JavaScript**: Modular, testable, documented, memory-efficient
|
||||
|
||||
[All original quality standards from v3 remain in effect]
|
||||
|
||||
## Migration Example: v3 to v4
|
||||
|
||||
**v3 Structure (Single File):**
|
||||
```
|
||||
ui_hybrid_1.html (contains everything)
|
||||
```
|
||||
|
||||
**v4 Structure (Modular):**
|
||||
```
|
||||
ui_hybrid_1/
|
||||
├── index.html (structure only)
|
||||
├── styles.css (all styling)
|
||||
└── script.js (all behavior)
|
||||
```
|
||||
|
||||
The same themed hybrid component now benefits from:
|
||||
- 3x better caching (each file cached independently)
|
||||
- Easier debugging (concerns separated)
|
||||
- Simpler version control (changes isolated to relevant files)
|
||||
- Team collaboration (parallel development possible)
|
||||
- Build tool ready (can be processed, minified, bundled)
|
||||
|
||||
## Iteration Evolution (Enhanced)
|
||||
|
||||
### **Architectural Sophistication** (New in v4)
|
||||
- **Foundation (1-3)**: Clean separation, basic modular structure
|
||||
- **Refinement (4-6)**: Advanced CSS architecture, sophisticated JS patterns
|
||||
- **Innovation (7+)**: Creative file communication, advanced state management
|
||||
|
||||
### **Development Complexity**
|
||||
- **Phase 1**: Standard separation with clear file boundaries
|
||||
- **Phase 2**: Advanced patterns like CSS custom properties + JS integration
|
||||
- **Phase 3**: Sophisticated architectures with event systems, style injection
|
||||
- **Phase 4**: Revolutionary approaches to component modularity
|
||||
|
||||
## Ultra-Thinking Directive (Enhanced)
|
||||
|
||||
Before each themed hybrid creation, deeply consider:
|
||||
|
||||
**Architectural Decisions:**
|
||||
- How can the three-file structure enhance this specific theme?
|
||||
- What belongs in CSS vs JavaScript for this component type?
|
||||
- How can files communicate elegantly for this use case?
|
||||
- What patterns best support this component's evolution?
|
||||
- How does separation improve maintainability here?
|
||||
|
||||
**File Responsibility Planning:**
|
||||
- What is the minimal, semantic HTML needed?
|
||||
- Which styles are structural vs thematic?
|
||||
- What JavaScript is essential vs enhancement?
|
||||
- How can each file remain focused and clean?
|
||||
- Where are the natural boundaries between concerns?
|
||||
|
||||
**Integration Excellence:**
|
||||
- How do the files work together seamlessly?
|
||||
- What naming conventions ensure clarity?
|
||||
- How can we avoid tight coupling?
|
||||
- What patterns enable future extensions?
|
||||
- How does the architecture support the theme?
|
||||
|
||||
[All original ultra-thinking directives from v3 remain relevant]
|
||||
|
||||
**Generate components that are:**
|
||||
- **Architecturally Sound**: Professional-grade file organization and separation
|
||||
- **Thematically Distinctive**: Strong design personality across all three files
|
||||
- **Functionally Integrated**: Multiple UI capabilities with clean code boundaries
|
||||
- **Professionally Crafted**: Industry-standard patterns and practices
|
||||
- **Immediately Impressive**: Excellence visible in both UI and code structure
|
||||
|
||||
The evolution from v3 to v4 represents growth from powerful prototypes to production-ready components, maintaining all creative excellence while adding architectural sophistication.
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Organic Search Hub - Nature-Inspired Discovery</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="search-hub" role="main">
|
||||
<header class="search-hub__header">
|
||||
<h1 class="search-hub__title">
|
||||
<span class="title-prefix">Discover</span>
|
||||
<span class="title-main">Nature's Answers</span>
|
||||
</h1>
|
||||
<p class="search-hub__subtitle">Let your search grow organically</p>
|
||||
</header>
|
||||
|
||||
<section class="search-container" role="search">
|
||||
<div class="search-input-wrapper">
|
||||
<div class="search-input-group">
|
||||
<input
|
||||
type="search"
|
||||
id="main-search"
|
||||
class="search-input"
|
||||
placeholder="Plant your search seed..."
|
||||
aria-label="Search"
|
||||
autocomplete="off"
|
||||
>
|
||||
<button class="search-button" aria-label="Search">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-controls">
|
||||
<button class="filter-toggle" aria-expanded="false" aria-controls="advanced-filters">
|
||||
<span class="filter-icon"></span>
|
||||
<span class="filter-text">Cultivate filters</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="advanced-filters" class="advanced-filters" aria-hidden="true">
|
||||
<fieldset class="filter-group">
|
||||
<legend>Growth Season</legend>
|
||||
<label class="filter-option">
|
||||
<input type="radio" name="timeframe" value="today">
|
||||
<span class="filter-label">Today's Sprouts</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<input type="radio" name="timeframe" value="week">
|
||||
<span class="filter-label">This Week's Bloom</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<input type="radio" name="timeframe" value="month">
|
||||
<span class="filter-label">Monthly Harvest</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<input type="radio" name="timeframe" value="all" checked>
|
||||
<span class="filter-label">Perennial Collection</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="filter-group">
|
||||
<legend>Garden Types</legend>
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" name="type" value="articles">
|
||||
<span class="filter-label">Knowledge Seeds</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" name="type" value="images">
|
||||
<span class="filter-label">Visual Blooms</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" name="type" value="videos">
|
||||
<span class="filter-label">Motion Vines</span>
|
||||
</label>
|
||||
<label class="filter-option">
|
||||
<input type="checkbox" name="type" value="audio">
|
||||
<span class="filter-label">Sound Leaves</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="search-suggestions" role="listbox" aria-label="Search suggestions">
|
||||
<!-- Dynamically populated suggestions -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="quick-actions" aria-label="Quick actions">
|
||||
<h2 class="section-title">Quick Roots</h2>
|
||||
<div class="action-grid">
|
||||
<button class="action-button" data-action="trending">
|
||||
<span class="action-icon action-icon--trending"></span>
|
||||
<span class="action-label">Trending Growth</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="seasonal">
|
||||
<span class="action-icon action-icon--seasonal"></span>
|
||||
<span class="action-label">Seasonal Picks</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="evergreen">
|
||||
<span class="action-icon action-icon--evergreen"></span>
|
||||
<span class="action-label">Evergreen Wisdom</span>
|
||||
</button>
|
||||
<button class="action-button" data-action="wild">
|
||||
<span class="action-icon action-icon--wild"></span>
|
||||
<span class="action-label">Wild Discovery</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="search-ecosystem">
|
||||
<div class="recent-searches">
|
||||
<h3 class="ecosystem-title">
|
||||
<span class="ecosystem-icon ecosystem-icon--recent"></span>
|
||||
Recent Plantings
|
||||
</h3>
|
||||
<ul class="search-list" role="list">
|
||||
<!-- Dynamically populated recent searches -->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="popular-searches">
|
||||
<h3 class="ecosystem-title">
|
||||
<span class="ecosystem-icon ecosystem-icon--popular"></span>
|
||||
Community Garden
|
||||
</h3>
|
||||
<ul class="search-list" role="list">
|
||||
<!-- Dynamically populated popular searches -->
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="search-hub__footer">
|
||||
<p class="growth-status">
|
||||
<span class="status-indicator"></span>
|
||||
<span class="status-text">Your search garden is thriving</span>
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,563 @@
|
|||
// Organic Search Hub - Interactive Functionality
|
||||
|
||||
class OrganicSearchHub {
|
||||
constructor() {
|
||||
this.searchInput = document.getElementById('main-search');
|
||||
this.searchButton = document.querySelector('.search-button');
|
||||
this.filterToggle = document.querySelector('.filter-toggle');
|
||||
this.advancedFilters = document.getElementById('advanced-filters');
|
||||
this.suggestionsContainer = document.querySelector('.search-suggestions');
|
||||
this.recentSearchesList = document.querySelector('.recent-searches .search-list');
|
||||
this.popularSearchesList = document.querySelector('.popular-searches .search-list');
|
||||
this.actionButtons = document.querySelectorAll('.action-button');
|
||||
this.statusText = document.querySelector('.status-text');
|
||||
|
||||
this.searchHistory = this.loadSearchHistory();
|
||||
this.currentSuggestions = [];
|
||||
this.debounceTimer = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.populateRecentSearches();
|
||||
this.populatePopularSearches();
|
||||
this.initializeFilters();
|
||||
this.startGrowthAnimation();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Search input events
|
||||
this.searchInput.addEventListener('input', (e) => this.handleSearchInput(e));
|
||||
this.searchInput.addEventListener('focus', () => this.showSuggestions());
|
||||
this.searchInput.addEventListener('keydown', (e) => this.handleKeyNavigation(e));
|
||||
|
||||
// Search button
|
||||
this.searchButton.addEventListener('click', () => this.performSearch());
|
||||
|
||||
// Filter toggle
|
||||
this.filterToggle.addEventListener('click', () => this.toggleFilters());
|
||||
|
||||
// Action buttons
|
||||
this.actionButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => this.handleQuickAction(e));
|
||||
});
|
||||
|
||||
// Click outside to close suggestions
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.search-container')) {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
});
|
||||
|
||||
// Filter changes
|
||||
this.advancedFilters.addEventListener('change', () => this.updateSearchContext());
|
||||
}
|
||||
|
||||
handleSearchInput(e) {
|
||||
const query = e.target.value.trim();
|
||||
|
||||
// Debounce search suggestions
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
if (query.length > 0) {
|
||||
this.generateSuggestions(query);
|
||||
this.showSuggestions();
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// Update growth status
|
||||
this.updateGrowthStatus(query);
|
||||
}
|
||||
|
||||
generateSuggestions(query) {
|
||||
// Simulated organic suggestions based on nature themes
|
||||
const natureTerms = [
|
||||
'sustainable gardens', 'permaculture design', 'native plants',
|
||||
'pollinator habitats', 'composting methods', 'seed saving',
|
||||
'organic farming', 'forest bathing', 'wildlife photography',
|
||||
'botanical illustration', 'mushroom foraging', 'tree identification',
|
||||
'bird watching', 'natural remedies', 'eco-friendly practices'
|
||||
];
|
||||
|
||||
// Filter and enhance suggestions
|
||||
const suggestions = natureTerms
|
||||
.filter(term => term.toLowerCase().includes(query.toLowerCase()))
|
||||
.slice(0, 5)
|
||||
.map(term => ({
|
||||
text: term,
|
||||
type: this.categorizeSuggestion(term),
|
||||
relevance: this.calculateRelevance(term, query)
|
||||
}))
|
||||
.sort((a, b) => b.relevance - a.relevance);
|
||||
|
||||
this.currentSuggestions = suggestions;
|
||||
this.renderSuggestions(suggestions);
|
||||
}
|
||||
|
||||
categorizeSuggestion(term) {
|
||||
const categories = {
|
||||
garden: ['garden', 'plant', 'seed', 'compost'],
|
||||
wildlife: ['bird', 'wildlife', 'pollinator', 'mushroom'],
|
||||
practice: ['sustainable', 'organic', 'eco-friendly', 'permaculture'],
|
||||
experience: ['bathing', 'watching', 'photography', 'illustration']
|
||||
};
|
||||
|
||||
for (const [category, keywords] of Object.entries(categories)) {
|
||||
if (keywords.some(keyword => term.toLowerCase().includes(keyword))) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
return 'general';
|
||||
}
|
||||
|
||||
calculateRelevance(term, query) {
|
||||
const lowerTerm = term.toLowerCase();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
// Exact match gets highest score
|
||||
if (lowerTerm === lowerQuery) return 100;
|
||||
|
||||
// Starting with query gets high score
|
||||
if (lowerTerm.startsWith(lowerQuery)) return 80;
|
||||
|
||||
// Contains query gets medium score
|
||||
if (lowerTerm.includes(lowerQuery)) return 60;
|
||||
|
||||
// Fuzzy match gets lower score
|
||||
return this.fuzzyMatch(lowerTerm, lowerQuery) ? 40 : 0;
|
||||
}
|
||||
|
||||
fuzzyMatch(str, pattern) {
|
||||
let patternIdx = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str[i] === pattern[patternIdx]) {
|
||||
patternIdx++;
|
||||
}
|
||||
if (patternIdx === pattern.length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
renderSuggestions(suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
const html = suggestions.map((suggestion, index) => `
|
||||
<div class="suggestion-item" data-index="${index}" role="option">
|
||||
<span class="suggestion-icon suggestion-icon--${suggestion.type}"></span>
|
||||
<span class="suggestion-text">${this.highlightMatch(suggestion.text)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
this.suggestionsContainer.innerHTML = html;
|
||||
|
||||
// Add click handlers to suggestions
|
||||
this.suggestionsContainer.querySelectorAll('.suggestion-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.dataset.index);
|
||||
this.selectSuggestion(this.currentSuggestions[index]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
highlightMatch(text) {
|
||||
const query = this.searchInput.value.trim();
|
||||
if (!query) return text;
|
||||
|
||||
const regex = new RegExp(`(${query})`, 'gi');
|
||||
return text.replace(regex, '<strong>$1</strong>');
|
||||
}
|
||||
|
||||
selectSuggestion(suggestion) {
|
||||
this.searchInput.value = suggestion.text;
|
||||
this.hideSuggestions();
|
||||
this.performSearch();
|
||||
}
|
||||
|
||||
showSuggestions() {
|
||||
if (this.currentSuggestions.length > 0) {
|
||||
this.suggestionsContainer.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
hideSuggestions() {
|
||||
this.suggestionsContainer.classList.remove('active');
|
||||
}
|
||||
|
||||
handleKeyNavigation(e) {
|
||||
const items = this.suggestionsContainer.querySelectorAll('.suggestion-item');
|
||||
const activeItem = this.suggestionsContainer.querySelector('.suggestion-item.active');
|
||||
let currentIndex = activeItem ? parseInt(activeItem.dataset.index) : -1;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
currentIndex = (currentIndex + 1) % items.length;
|
||||
this.highlightSuggestion(currentIndex);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
currentIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
|
||||
this.highlightSuggestion(currentIndex);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
if (currentIndex >= 0) {
|
||||
e.preventDefault();
|
||||
this.selectSuggestion(this.currentSuggestions[currentIndex]);
|
||||
} else {
|
||||
this.performSearch();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
this.hideSuggestions();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
highlightSuggestion(index) {
|
||||
const items = this.suggestionsContainer.querySelectorAll('.suggestion-item');
|
||||
items.forEach(item => item.classList.remove('active'));
|
||||
|
||||
if (index >= 0 && index < items.length) {
|
||||
items[index].classList.add('active');
|
||||
items[index].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilters() {
|
||||
const isExpanded = this.filterToggle.getAttribute('aria-expanded') === 'true';
|
||||
this.filterToggle.setAttribute('aria-expanded', !isExpanded);
|
||||
this.advancedFilters.setAttribute('aria-hidden', isExpanded);
|
||||
|
||||
if (!isExpanded) {
|
||||
this.advancedFilters.style.display = 'grid';
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
this.advancedFilters.style.transform = 'scaleY(1)';
|
||||
this.advancedFilters.style.opacity = '1';
|
||||
});
|
||||
} else {
|
||||
this.advancedFilters.style.transform = 'scaleY(0)';
|
||||
this.advancedFilters.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
this.advancedFilters.style.display = 'none';
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
performSearch() {
|
||||
const query = this.searchInput.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
// Add to search history
|
||||
this.addToSearchHistory(query);
|
||||
|
||||
// Get active filters
|
||||
const filters = this.getActiveFilters();
|
||||
|
||||
// Simulate search action
|
||||
console.log('Searching for:', query, 'with filters:', filters);
|
||||
|
||||
// Update UI feedback
|
||||
this.showSearchFeedback(query);
|
||||
|
||||
// Clear suggestions
|
||||
this.hideSuggestions();
|
||||
}
|
||||
|
||||
getActiveFilters() {
|
||||
const filters = {
|
||||
timeframe: null,
|
||||
types: []
|
||||
};
|
||||
|
||||
// Get timeframe
|
||||
const timeframeInput = this.advancedFilters.querySelector('input[name="timeframe"]:checked');
|
||||
if (timeframeInput) {
|
||||
filters.timeframe = timeframeInput.value;
|
||||
}
|
||||
|
||||
// Get types
|
||||
const typeInputs = this.advancedFilters.querySelectorAll('input[name="type"]:checked');
|
||||
typeInputs.forEach(input => {
|
||||
filters.types.push(input.value);
|
||||
});
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
handleQuickAction(e) {
|
||||
const button = e.currentTarget;
|
||||
const action = button.dataset.action;
|
||||
|
||||
// Animate button
|
||||
button.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 200);
|
||||
|
||||
// Perform action
|
||||
switch(action) {
|
||||
case 'trending':
|
||||
this.searchInput.value = 'trending in sustainable living';
|
||||
break;
|
||||
case 'seasonal':
|
||||
this.searchInput.value = this.getSeasonalQuery();
|
||||
break;
|
||||
case 'evergreen':
|
||||
this.searchInput.value = 'timeless gardening wisdom';
|
||||
break;
|
||||
case 'wild':
|
||||
this.searchInput.value = this.getRandomNatureQuery();
|
||||
break;
|
||||
}
|
||||
|
||||
this.performSearch();
|
||||
}
|
||||
|
||||
getSeasonalQuery() {
|
||||
const month = new Date().getMonth();
|
||||
const seasons = {
|
||||
spring: 'spring planting guide',
|
||||
summer: 'summer garden care',
|
||||
fall: 'autumn harvest tips',
|
||||
winter: 'winter garden preparation'
|
||||
};
|
||||
|
||||
if (month >= 2 && month <= 4) return seasons.spring;
|
||||
if (month >= 5 && month <= 7) return seasons.summer;
|
||||
if (month >= 8 && month <= 10) return seasons.fall;
|
||||
return seasons.winter;
|
||||
}
|
||||
|
||||
getRandomNatureQuery() {
|
||||
const queries = [
|
||||
'rare botanical species',
|
||||
'unexplored nature trails',
|
||||
'unique ecosystem discoveries',
|
||||
'hidden garden gems',
|
||||
'secret foraging spots'
|
||||
];
|
||||
return queries[Math.floor(Math.random() * queries.length)];
|
||||
}
|
||||
|
||||
addToSearchHistory(query) {
|
||||
// Remove if already exists
|
||||
this.searchHistory = this.searchHistory.filter(item => item !== query);
|
||||
|
||||
// Add to beginning
|
||||
this.searchHistory.unshift(query);
|
||||
|
||||
// Keep only last 10
|
||||
this.searchHistory = this.searchHistory.slice(0, 10);
|
||||
|
||||
// Save to localStorage
|
||||
this.saveSearchHistory();
|
||||
|
||||
// Update UI
|
||||
this.populateRecentSearches();
|
||||
}
|
||||
|
||||
loadSearchHistory() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('organicSearchHistory')) || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
saveSearchHistory() {
|
||||
try {
|
||||
localStorage.setItem('organicSearchHistory', JSON.stringify(this.searchHistory));
|
||||
} catch (e) {
|
||||
console.error('Failed to save search history:', e);
|
||||
}
|
||||
}
|
||||
|
||||
populateRecentSearches() {
|
||||
if (this.searchHistory.length === 0) {
|
||||
this.recentSearchesList.innerHTML = '<li class="empty-state">No recent searches yet</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = this.searchHistory.slice(0, 5).map(query =>
|
||||
`<li data-query="${query}">${query}</li>`
|
||||
).join('');
|
||||
|
||||
this.recentSearchesList.innerHTML = html;
|
||||
|
||||
// Add click handlers
|
||||
this.recentSearchesList.querySelectorAll('li').forEach(item => {
|
||||
if (!item.classList.contains('empty-state')) {
|
||||
item.addEventListener('click', () => {
|
||||
this.searchInput.value = item.dataset.query;
|
||||
this.performSearch();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
populatePopularSearches() {
|
||||
const popularSearches = [
|
||||
'composting basics',
|
||||
'native plant species',
|
||||
'butterfly gardens',
|
||||
'organic pest control',
|
||||
'rainwater harvesting'
|
||||
];
|
||||
|
||||
const html = popularSearches.map(query =>
|
||||
`<li data-query="${query}">${query}</li>`
|
||||
).join('');
|
||||
|
||||
this.popularSearchesList.innerHTML = html;
|
||||
|
||||
// Add click handlers
|
||||
this.popularSearchesList.querySelectorAll('li').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
this.searchInput.value = item.dataset.query;
|
||||
this.performSearch();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateSearchContext() {
|
||||
const filters = this.getActiveFilters();
|
||||
let context = 'Searching ';
|
||||
|
||||
if (filters.timeframe && filters.timeframe !== 'all') {
|
||||
context += `${filters.timeframe}'s `;
|
||||
}
|
||||
|
||||
if (filters.types.length > 0) {
|
||||
context += filters.types.join(', ');
|
||||
} else {
|
||||
context += 'all content';
|
||||
}
|
||||
|
||||
this.showStatusMessage(context);
|
||||
}
|
||||
|
||||
updateGrowthStatus(query) {
|
||||
const messages = [
|
||||
'Your search is taking root...',
|
||||
'Ideas are sprouting...',
|
||||
'Knowledge is blooming...',
|
||||
'Wisdom is growing...',
|
||||
'Discoveries are flourishing...'
|
||||
];
|
||||
|
||||
if (query.length > 0) {
|
||||
const index = Math.min(Math.floor(query.length / 3), messages.length - 1);
|
||||
this.statusText.textContent = messages[index];
|
||||
} else {
|
||||
this.statusText.textContent = 'Your search garden is thriving';
|
||||
}
|
||||
}
|
||||
|
||||
showSearchFeedback(query) {
|
||||
const messages = [
|
||||
`Cultivating results for "${query}"...`,
|
||||
`Harvesting knowledge about "${query}"...`,
|
||||
`Growing insights on "${query}"...`,
|
||||
`Nurturing information about "${query}"...`
|
||||
];
|
||||
|
||||
const message = messages[Math.floor(Math.random() * messages.length)];
|
||||
this.showStatusMessage(message);
|
||||
|
||||
// Reset after delay
|
||||
setTimeout(() => {
|
||||
this.statusText.textContent = 'Your search garden is thriving';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showStatusMessage(message) {
|
||||
this.statusText.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
this.statusText.textContent = message;
|
||||
this.statusText.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
initializeFilters() {
|
||||
// Set initial state
|
||||
this.advancedFilters.style.transform = 'scaleY(0)';
|
||||
this.advancedFilters.style.opacity = '0';
|
||||
this.advancedFilters.style.transformOrigin = 'top';
|
||||
this.advancedFilters.style.transition = 'all 400ms cubic-bezier(0.34, 1.56, 0.64, 1)';
|
||||
}
|
||||
|
||||
startGrowthAnimation() {
|
||||
// Animate elements on load
|
||||
const elements = [
|
||||
{ el: '.search-hub__header', delay: 0 },
|
||||
{ el: '.search-container', delay: 200 },
|
||||
{ el: '.quick-actions', delay: 400 },
|
||||
{ el: '.search-ecosystem', delay: 600 }
|
||||
];
|
||||
|
||||
elements.forEach(({ el, delay }) => {
|
||||
const element = document.querySelector(el);
|
||||
if (element) {
|
||||
element.style.opacity = '0';
|
||||
element.style.transform = 'translateY(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transition = 'all 800ms ease-out';
|
||||
element.style.opacity = '1';
|
||||
element.style.transform = 'translateY(0)';
|
||||
}, delay);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new OrganicSearchHub();
|
||||
});
|
||||
|
||||
// Add custom styles for suggestion type icons
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.suggestion-icon--garden::after { content: '🌱'; }
|
||||
.suggestion-icon--wildlife::after { content: '🦋'; }
|
||||
.suggestion-icon--practice::after { content: '♻️'; }
|
||||
.suggestion-icon--experience::after { content: '🌄'; }
|
||||
.suggestion-icon--general::after { content: '🔍'; }
|
||||
|
||||
.suggestion-icon::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.suggestion-item.active {
|
||||
background: var(--morning-mist);
|
||||
padding-left: var(--space-branch);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.empty-state:hover {
|
||||
transform: none !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
|
@ -0,0 +1,628 @@
|
|||
/* Organic Nature Theme - Search Hub Styles */
|
||||
|
||||
:root {
|
||||
/* Nature-inspired color palette */
|
||||
--moss-dark: #2d4a2b;
|
||||
--moss-medium: #4a6741;
|
||||
--moss-light: #6b8e4e;
|
||||
--sage-green: #87a96b;
|
||||
--spring-green: #a8c09a;
|
||||
--leaf-accent: #7fb069;
|
||||
--bark-brown: #6f4e37;
|
||||
--soil-dark: #3e2723;
|
||||
--sand-light: #f5e6d3;
|
||||
--morning-mist: #e8f5e9;
|
||||
--sky-blue: #87ceeb;
|
||||
--pollen-yellow: #f7dc6f;
|
||||
--berry-red: #d32f2f;
|
||||
|
||||
/* Organic spacing rhythm */
|
||||
--space-seed: 0.25rem;
|
||||
--space-sprout: 0.5rem;
|
||||
--space-stem: 1rem;
|
||||
--space-branch: 1.5rem;
|
||||
--space-canopy: 2.5rem;
|
||||
--space-forest: 4rem;
|
||||
|
||||
/* Natural transitions */
|
||||
--transition-breeze: 200ms ease-out;
|
||||
--transition-growth: 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--transition-season: 600ms ease-in-out;
|
||||
|
||||
/* Organic shapes */
|
||||
--radius-pebble: 0.25rem;
|
||||
--radius-stone: 0.5rem;
|
||||
--radius-boulder: 1rem;
|
||||
--radius-hill: 2rem;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, var(--morning-mist) 0%, var(--sand-light) 100%);
|
||||
color: var(--soil-dark);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Organic background pattern */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 30%, var(--spring-green) 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 70%, var(--sage-green) 0%, transparent 30%),
|
||||
radial-gradient(circle at 40% 90%, var(--leaf-accent) 0%, transparent 25%);
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
animation: organicFloat 30s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes organicFloat {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(-20px, -30px) scale(1.05); }
|
||||
50% { transform: translate(30px, -20px) scale(0.98); }
|
||||
75% { transform: translate(-10px, 20px) scale(1.02); }
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.search-hub {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-branch) var(--space-stem);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.search-hub__header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-canopy);
|
||||
animation: bloomIn 800ms ease-out;
|
||||
}
|
||||
|
||||
.search-hub__title {
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 300;
|
||||
color: var(--moss-dark);
|
||||
margin-bottom: var(--space-sprout);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-seed);
|
||||
}
|
||||
|
||||
.title-prefix {
|
||||
font-size: 0.6em;
|
||||
color: var(--moss-medium);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title-main {
|
||||
background: linear-gradient(135deg, var(--moss-dark), var(--leaf-accent));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.search-hub__subtitle {
|
||||
color: var(--moss-medium);
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Search container */
|
||||
.search-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: var(--radius-hill);
|
||||
padding: var(--space-canopy);
|
||||
box-shadow:
|
||||
0 10px 40px rgba(46, 74, 43, 0.1),
|
||||
0 2px 10px rgba(46, 74, 43, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
margin-bottom: var(--space-canopy);
|
||||
}
|
||||
|
||||
/* Search input group */
|
||||
.search-input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--morning-mist);
|
||||
border-radius: var(--radius-boulder);
|
||||
border: 2px solid transparent;
|
||||
transition: all var(--transition-growth);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input-group:focus-within {
|
||||
border-color: var(--leaf-accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 5px 20px rgba(127, 176, 105, 0.3),
|
||||
0 2px 8px rgba(127, 176, 105, 0.2);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: var(--space-stem) var(--space-branch);
|
||||
font-size: 1.125rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--soil-dark);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--moss-medium);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding: var(--space-stem) var(--space-branch);
|
||||
background: var(--leaf-accent);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
transition: all var(--transition-breeze);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background: var(--moss-light);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
/* Filter controls */
|
||||
.filter-controls {
|
||||
margin-top: var(--space-stem);
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sprout);
|
||||
padding: var(--space-sprout) var(--space-stem);
|
||||
background: transparent;
|
||||
border: 2px solid var(--spring-green);
|
||||
border-radius: var(--radius-stone);
|
||||
color: var(--moss-dark);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-breeze);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.filter-toggle:hover {
|
||||
background: var(--spring-green);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--moss-medium);
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 4h18v2.172a2 2 0 0 1-.586 1.414l-6.828 6.828A2 2 0 0 0 13 15.828V20l-4 2v-6.172a2 2 0 0 0-.586-1.414L1.586 7.586A2 2 0 0 1 1 6.172V4z'/%3E%3C/svg%3E") center/contain no-repeat;
|
||||
transition: transform var(--transition-breeze);
|
||||
}
|
||||
|
||||
.filter-toggle[aria-expanded="true"] .filter-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Advanced filters */
|
||||
.advanced-filters {
|
||||
margin-top: var(--space-branch);
|
||||
padding: var(--space-branch);
|
||||
background: rgba(232, 245, 233, 0.5);
|
||||
border-radius: var(--radius-boulder);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--space-branch);
|
||||
transform-origin: top;
|
||||
transition: all var(--transition-growth);
|
||||
}
|
||||
|
||||
.advanced-filters[aria-hidden="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-group legend {
|
||||
font-weight: 600;
|
||||
color: var(--moss-dark);
|
||||
margin-bottom: var(--space-sprout);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sprout);
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-sprout);
|
||||
margin-bottom: var(--space-seed);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-stone);
|
||||
transition: all var(--transition-breeze);
|
||||
}
|
||||
|
||||
.filter-option:hover {
|
||||
background: rgba(127, 176, 105, 0.1);
|
||||
}
|
||||
|
||||
.filter-option input {
|
||||
margin-right: var(--space-sprout);
|
||||
accent-color: var(--leaf-accent);
|
||||
}
|
||||
|
||||
/* Search suggestions */
|
||||
.search-suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: var(--radius-boulder);
|
||||
box-shadow: 0 10px 30px rgba(46, 74, 43, 0.15);
|
||||
margin-top: var(--space-sprout);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.search-suggestions.active {
|
||||
display: block;
|
||||
animation: growDown 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes growDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: var(--space-stem);
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--morning-mist);
|
||||
transition: all var(--transition-breeze);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sprout);
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: var(--morning-mist);
|
||||
padding-left: var(--space-branch);
|
||||
}
|
||||
|
||||
.suggestion-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--spring-green);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Quick actions */
|
||||
.quick-actions {
|
||||
margin-bottom: var(--space-canopy);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--moss-dark);
|
||||
margin-bottom: var(--space-branch);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-stem);
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--leaf-accent), transparent);
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-stem);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-sprout);
|
||||
padding: var(--space-branch);
|
||||
background: white;
|
||||
border: 2px solid var(--spring-green);
|
||||
border-radius: var(--radius-boulder);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-growth);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.action-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at center, var(--leaf-accent), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-breeze);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 25px rgba(127, 176, 105, 0.3);
|
||||
border-color: var(--leaf-accent);
|
||||
}
|
||||
|
||||
.action-button:hover::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--moss-light);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-icon--trending::after {
|
||||
content: '📈';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.action-icon--seasonal::after {
|
||||
content: '🌿';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.action-icon--evergreen::after {
|
||||
content: '🌲';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.action-icon--wild::after {
|
||||
content: '🦋';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-weight: 500;
|
||||
color: var(--moss-dark);
|
||||
}
|
||||
|
||||
/* Search ecosystem */
|
||||
.search-ecosystem {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--space-branch);
|
||||
margin-bottom: var(--space-canopy);
|
||||
}
|
||||
|
||||
.recent-searches,
|
||||
.popular-searches {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: var(--radius-boulder);
|
||||
padding: var(--space-branch);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.ecosystem-title {
|
||||
font-size: 1.125rem;
|
||||
color: var(--moss-dark);
|
||||
margin-bottom: var(--space-stem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sprout);
|
||||
}
|
||||
|
||||
.ecosystem-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--spring-green);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ecosystem-icon--recent::after {
|
||||
content: '🕐';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ecosystem-icon--popular::after {
|
||||
content: '⭐';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.search-list li {
|
||||
padding: var(--space-sprout) 0;
|
||||
border-bottom: 1px solid var(--morning-mist);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-breeze);
|
||||
position: relative;
|
||||
padding-left: var(--space-branch);
|
||||
}
|
||||
|
||||
.search-list li::before {
|
||||
content: '🌱';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
transition: all var(--transition-breeze);
|
||||
}
|
||||
|
||||
.search-list li:hover {
|
||||
color: var(--leaf-accent);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.search-list li:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.search-hub__footer {
|
||||
text-align: center;
|
||||
padding: var(--space-branch) 0;
|
||||
}
|
||||
|
||||
.growth-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sprout);
|
||||
padding: var(--space-sprout) var(--space-branch);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: var(--radius-hill);
|
||||
color: var(--moss-medium);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--leaf-accent);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.2); opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes bloomIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.search-hub {
|
||||
padding: var(--space-stem);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: var(--space-branch);
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.search-ecosystem {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
:focus-visible {
|
||||
outline: 3px solid var(--leaf-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--morning-mist);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--moss-light);
|
||||
border-radius: var(--radius-stone);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--moss-medium);
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Digital Minimalism - Input Intelligence</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">Input Intelligence</h1>
|
||||
<p class="subtitle">Smart form interactions with minimal design</p>
|
||||
</header>
|
||||
|
||||
<section class="input-section">
|
||||
<form class="intelligent-form" id="smartForm">
|
||||
<div class="input-group" data-input-type="email">
|
||||
<label for="emailInput" class="input-label">Email Address</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="emailInput"
|
||||
class="intelligent-input"
|
||||
placeholder="Type your email"
|
||||
autocomplete="off"
|
||||
aria-describedby="emailHint emailError"
|
||||
>
|
||||
<div class="input-progress" aria-hidden="true"></div>
|
||||
<div class="input-indicator" aria-hidden="true"></div>
|
||||
</div>
|
||||
<p id="emailHint" class="input-hint">We'll validate as you type</p>
|
||||
<p id="emailError" class="input-error" role="alert" aria-live="polite"></p>
|
||||
<ul class="suggestions-list" id="emailSuggestions" role="listbox" aria-label="Email suggestions"></ul>
|
||||
</div>
|
||||
|
||||
<div class="input-group" data-input-type="phone">
|
||||
<label for="phoneInput" class="input-label">Phone Number</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
type="tel"
|
||||
id="phoneInput"
|
||||
class="intelligent-input"
|
||||
placeholder="(123) 456-7890"
|
||||
autocomplete="off"
|
||||
aria-describedby="phoneHint phoneError"
|
||||
>
|
||||
<div class="input-progress" aria-hidden="true"></div>
|
||||
<div class="input-indicator" aria-hidden="true"></div>
|
||||
</div>
|
||||
<p id="phoneHint" class="input-hint">Auto-formatting enabled</p>
|
||||
<p id="phoneError" class="input-error" role="alert" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<div class="input-group" data-input-type="card">
|
||||
<label for="cardInput" class="input-label">Credit Card</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="cardInput"
|
||||
class="intelligent-input"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
autocomplete="off"
|
||||
aria-describedby="cardHint cardError"
|
||||
>
|
||||
<div class="input-progress" aria-hidden="true"></div>
|
||||
<div class="input-indicator" aria-hidden="true"></div>
|
||||
<div class="card-type" aria-label="Card type"></div>
|
||||
</div>
|
||||
<p id="cardHint" class="input-hint">Secure input with automatic spacing</p>
|
||||
<p id="cardError" class="input-error" role="alert" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<div class="input-group" data-input-type="password">
|
||||
<label for="passwordInput" class="input-label">Secure Password</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
type="password"
|
||||
id="passwordInput"
|
||||
class="intelligent-input"
|
||||
placeholder="Create a strong password"
|
||||
autocomplete="new-password"
|
||||
aria-describedby="passwordHint passwordError passwordStrength"
|
||||
>
|
||||
<div class="input-progress" aria-hidden="true"></div>
|
||||
<button type="button" class="password-toggle" aria-label="Toggle password visibility">
|
||||
<span class="password-toggle-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p id="passwordHint" class="input-hint">Minimum 8 characters</p>
|
||||
<div id="passwordStrength" class="password-strength" role="status" aria-live="polite">
|
||||
<div class="strength-meter">
|
||||
<div class="strength-fill"></div>
|
||||
</div>
|
||||
<span class="strength-text"></span>
|
||||
</div>
|
||||
<p id="passwordError" class="input-error" role="alert" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="progress-text">0% Complete</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-button" disabled>
|
||||
<span class="button-text">Submit Form</span>
|
||||
<span class="button-loader"></span>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="validFields">0</span>
|
||||
<span class="stat-label">Valid Fields</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="totalFields">4</span>
|
||||
<span class="stat-label">Total Fields</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="inputQuality">0%</span>
|
||||
<span class="stat-label">Input Quality</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
// Digital Minimalism - Input Intelligence Script
|
||||
|
||||
class InputIntelligence {
|
||||
constructor() {
|
||||
this.form = document.getElementById('smartForm');
|
||||
this.inputs = {};
|
||||
this.validators = {
|
||||
email: this.validateEmail.bind(this),
|
||||
phone: this.validatePhone.bind(this),
|
||||
card: this.validateCard.bind(this),
|
||||
password: this.validatePassword.bind(this)
|
||||
};
|
||||
this.formatters = {
|
||||
phone: this.formatPhone.bind(this),
|
||||
card: this.formatCard.bind(this)
|
||||
};
|
||||
this.validFields = new Set();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize all input fields
|
||||
this.form.querySelectorAll('.intelligent-input').forEach(input => {
|
||||
const type = input.closest('.input-group').dataset.inputType;
|
||||
this.inputs[type] = {
|
||||
element: input,
|
||||
group: input.closest('.input-group'),
|
||||
error: input.closest('.input-group').querySelector('.input-error'),
|
||||
hint: input.closest('.input-group').querySelector('.input-hint'),
|
||||
isValid: false
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
input.addEventListener('input', (e) => this.handleInput(e, type));
|
||||
input.addEventListener('focus', (e) => this.handleFocus(e, type));
|
||||
input.addEventListener('blur', (e) => this.handleBlur(e, type));
|
||||
input.addEventListener('keydown', (e) => this.handleKeydown(e, type));
|
||||
});
|
||||
|
||||
// Initialize email suggestions
|
||||
this.initEmailSuggestions();
|
||||
|
||||
// Initialize password toggle
|
||||
this.initPasswordToggle();
|
||||
|
||||
// Form submission
|
||||
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
|
||||
// Update initial stats
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
handleInput(event, type) {
|
||||
const input = event.target;
|
||||
const value = input.value;
|
||||
|
||||
// Apply formatter if available
|
||||
if (this.formatters[type]) {
|
||||
const formatted = this.formatters[type](value);
|
||||
if (formatted !== value) {
|
||||
input.value = formatted;
|
||||
// Restore cursor position
|
||||
const cursorPos = this.getCursorPosition(input, value, formatted);
|
||||
input.setSelectionRange(cursorPos, cursorPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate input
|
||||
this.validateField(type, input.value);
|
||||
|
||||
// Update progress
|
||||
this.updateProgress();
|
||||
|
||||
// Type-specific handlers
|
||||
if (type === 'email') {
|
||||
this.updateEmailSuggestions(input.value);
|
||||
} else if (type === 'card') {
|
||||
this.updateCardType(input.value);
|
||||
} else if (type === 'password') {
|
||||
this.updatePasswordStrength(input.value);
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus(event, type) {
|
||||
const group = this.inputs[type].group;
|
||||
group.classList.add('active');
|
||||
|
||||
// Show suggestions for email
|
||||
if (type === 'email') {
|
||||
const suggestions = document.getElementById('emailSuggestions');
|
||||
if (suggestions.children.length > 0) {
|
||||
suggestions.classList.add('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur(event, type) {
|
||||
const group = this.inputs[type].group;
|
||||
group.classList.remove('active');
|
||||
|
||||
// Hide suggestions
|
||||
if (type === 'email') {
|
||||
setTimeout(() => {
|
||||
document.getElementById('emailSuggestions').classList.remove('visible');
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(event, type) {
|
||||
if (type === 'email' && (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter')) {
|
||||
this.handleSuggestionNavigation(event);
|
||||
}
|
||||
}
|
||||
|
||||
validateField(type, value) {
|
||||
const validator = this.validators[type];
|
||||
const input = this.inputs[type];
|
||||
|
||||
if (!validator) return;
|
||||
|
||||
const validation = validator(value);
|
||||
|
||||
input.isValid = validation.isValid;
|
||||
input.group.classList.toggle('valid', validation.isValid);
|
||||
input.group.classList.toggle('invalid', !validation.isValid && value.length > 0);
|
||||
|
||||
// Update error message
|
||||
if (!validation.isValid && value.length > 0) {
|
||||
input.error.textContent = validation.error;
|
||||
input.error.classList.add('visible');
|
||||
input.hint.style.opacity = '0';
|
||||
} else {
|
||||
input.error.classList.remove('visible');
|
||||
input.hint.style.opacity = '1';
|
||||
}
|
||||
|
||||
// Update valid fields set
|
||||
if (validation.isValid) {
|
||||
this.validFields.add(type);
|
||||
} else {
|
||||
this.validFields.delete(type);
|
||||
}
|
||||
|
||||
// Update submit button state
|
||||
this.updateSubmitButton();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
validateEmail(email) {
|
||||
if (!email) return { isValid: false, error: '' };
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const isValid = emailRegex.test(email);
|
||||
|
||||
return {
|
||||
isValid,
|
||||
error: isValid ? '' : 'Please enter a valid email address'
|
||||
};
|
||||
}
|
||||
|
||||
validatePhone(phone) {
|
||||
if (!phone) return { isValid: false, error: '' };
|
||||
|
||||
const cleaned = phone.replace(/\D/g, '');
|
||||
const isValid = cleaned.length === 10;
|
||||
|
||||
return {
|
||||
isValid,
|
||||
error: isValid ? '' : 'Please enter a 10-digit phone number'
|
||||
};
|
||||
}
|
||||
|
||||
validateCard(card) {
|
||||
if (!card) return { isValid: false, error: '' };
|
||||
|
||||
const cleaned = card.replace(/\s/g, '');
|
||||
const isValid = cleaned.length >= 13 && cleaned.length <= 19 && this.luhnCheck(cleaned);
|
||||
|
||||
return {
|
||||
isValid,
|
||||
error: isValid ? '' : 'Please enter a valid card number'
|
||||
};
|
||||
}
|
||||
|
||||
validatePassword(password) {
|
||||
if (!password) return { isValid: false, error: '' };
|
||||
|
||||
const checks = {
|
||||
length: password.length >= 8,
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
lowercase: /[a-z]/.test(password),
|
||||
number: /\d/.test(password),
|
||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||
};
|
||||
|
||||
const passedChecks = Object.values(checks).filter(Boolean).length;
|
||||
const isValid = passedChecks >= 3 && checks.length;
|
||||
|
||||
let error = '';
|
||||
if (!checks.length) error = 'Password must be at least 8 characters';
|
||||
else if (passedChecks < 3) error = 'Password needs more complexity';
|
||||
|
||||
return { isValid, error, strength: passedChecks };
|
||||
}
|
||||
|
||||
formatPhone(value) {
|
||||
const cleaned = value.replace(/\D/g, '');
|
||||
|
||||
if (cleaned.length <= 3) return cleaned;
|
||||
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
|
||||
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
|
||||
}
|
||||
|
||||
formatCard(value) {
|
||||
const cleaned = value.replace(/\s/g, '');
|
||||
const parts = [];
|
||||
|
||||
for (let i = 0; i < cleaned.length; i += 4) {
|
||||
parts.push(cleaned.slice(i, i + 4));
|
||||
}
|
||||
|
||||
return parts.join(' ').trim();
|
||||
}
|
||||
|
||||
luhnCheck(value) {
|
||||
let sum = 0;
|
||||
let isEven = false;
|
||||
|
||||
for (let i = value.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(value[i]);
|
||||
|
||||
if (isEven) {
|
||||
digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
}
|
||||
|
||||
sum += digit;
|
||||
isEven = !isEven;
|
||||
}
|
||||
|
||||
return sum % 10 === 0;
|
||||
}
|
||||
|
||||
getCursorPosition(input, oldValue, newValue) {
|
||||
const oldPos = input.selectionStart;
|
||||
const oldLength = oldValue.length;
|
||||
const newLength = newValue.length;
|
||||
|
||||
if (oldLength < newLength) {
|
||||
// Characters were added
|
||||
return oldPos + (newLength - oldLength);
|
||||
}
|
||||
return oldPos;
|
||||
}
|
||||
|
||||
initEmailSuggestions() {
|
||||
const emailInput = this.inputs.email.element;
|
||||
const suggestionsList = document.getElementById('emailSuggestions');
|
||||
|
||||
this.emailDomains = ['gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com'];
|
||||
this.selectedSuggestion = -1;
|
||||
}
|
||||
|
||||
updateEmailSuggestions(value) {
|
||||
const suggestionsList = document.getElementById('emailSuggestions');
|
||||
suggestionsList.innerHTML = '';
|
||||
this.selectedSuggestion = -1;
|
||||
|
||||
if (!value.includes('@') || value.endsWith('@')) {
|
||||
const localPart = value.split('@')[0];
|
||||
if (localPart.length > 0) {
|
||||
this.emailDomains.forEach(domain => {
|
||||
const suggestion = `${localPart}@${domain}`;
|
||||
const li = document.createElement('li');
|
||||
li.className = 'suggestion-item';
|
||||
li.textContent = suggestion;
|
||||
li.setAttribute('role', 'option');
|
||||
li.addEventListener('click', () => {
|
||||
this.inputs.email.element.value = suggestion;
|
||||
this.validateField('email', suggestion);
|
||||
suggestionsList.classList.remove('visible');
|
||||
});
|
||||
suggestionsList.appendChild(li);
|
||||
});
|
||||
suggestionsList.classList.add('visible');
|
||||
}
|
||||
} else {
|
||||
suggestionsList.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
handleSuggestionNavigation(event) {
|
||||
const suggestionsList = document.getElementById('emailSuggestions');
|
||||
const suggestions = suggestionsList.querySelectorAll('.suggestion-item');
|
||||
|
||||
if (suggestions.length === 0) return;
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.selectedSuggestion = Math.min(this.selectedSuggestion + 1, suggestions.length - 1);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.selectedSuggestion = Math.max(this.selectedSuggestion - 1, -1);
|
||||
} else if (event.key === 'Enter' && this.selectedSuggestion >= 0) {
|
||||
event.preventDefault();
|
||||
suggestions[this.selectedSuggestion].click();
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.forEach((item, index) => {
|
||||
item.classList.toggle('selected', index === this.selectedSuggestion);
|
||||
});
|
||||
}
|
||||
|
||||
updateCardType(value) {
|
||||
const cardType = this.inputs.card.group.querySelector('.card-type');
|
||||
const cleaned = value.replace(/\s/g, '');
|
||||
|
||||
let type = '';
|
||||
if (cleaned.startsWith('4')) type = 'VISA';
|
||||
else if (cleaned.startsWith('5')) type = 'MC';
|
||||
else if (cleaned.startsWith('3')) type = 'AMEX';
|
||||
else if (cleaned.startsWith('6')) type = 'DISC';
|
||||
|
||||
if (type) {
|
||||
cardType.textContent = type;
|
||||
cardType.classList.add('visible');
|
||||
} else {
|
||||
cardType.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
initPasswordToggle() {
|
||||
const toggle = this.form.querySelector('.password-toggle');
|
||||
const passwordInput = this.inputs.password.element;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const isPassword = passwordInput.type === 'password';
|
||||
passwordInput.type = isPassword ? 'text' : 'password';
|
||||
toggle.classList.toggle('visible', !isPassword);
|
||||
});
|
||||
}
|
||||
|
||||
updatePasswordStrength(value) {
|
||||
const strengthContainer = document.getElementById('passwordStrength');
|
||||
const strengthFill = strengthContainer.querySelector('.strength-fill');
|
||||
const strengthText = strengthContainer.querySelector('.strength-text');
|
||||
|
||||
if (!value) {
|
||||
strengthContainer.classList.remove('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = this.validatePassword(value);
|
||||
const strength = validation.strength || 0;
|
||||
|
||||
strengthContainer.classList.add('visible');
|
||||
strengthFill.style.width = `${(strength / 5) * 100}%`;
|
||||
|
||||
const strengthLabels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'];
|
||||
strengthText.textContent = strengthLabels[strength - 1] || 'Very Weak';
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
const totalFields = Object.keys(this.inputs).length;
|
||||
const filledFields = Object.values(this.inputs).filter(input => input.element.value.length > 0).length;
|
||||
const validFields = this.validFields.size;
|
||||
|
||||
const progress = (validFields / totalFields) * 100;
|
||||
const progressFill = this.form.querySelector('.progress-fill');
|
||||
const progressText = this.form.querySelector('.progress-text');
|
||||
|
||||
progressFill.style.width = `${progress}%`;
|
||||
progressText.textContent = `${Math.round(progress)}% Complete`;
|
||||
}
|
||||
|
||||
updateSubmitButton() {
|
||||
const submitButton = this.form.querySelector('.submit-button');
|
||||
const allValid = this.validFields.size === Object.keys(this.inputs).length;
|
||||
submitButton.disabled = !allValid;
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
document.getElementById('validFields').textContent = this.validFields.size;
|
||||
document.getElementById('totalFields').textContent = Object.keys(this.inputs).length;
|
||||
|
||||
const quality = (this.validFields.size / Object.keys(this.inputs).length) * 100;
|
||||
document.getElementById('inputQuality').textContent = `${Math.round(quality)}%`;
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const submitButton = this.form.querySelector('.submit-button');
|
||||
submitButton.classList.add('loading');
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Collect form data
|
||||
const formData = {};
|
||||
Object.entries(this.inputs).forEach(([type, input]) => {
|
||||
formData[type] = input.element.value;
|
||||
});
|
||||
|
||||
console.log('Form submitted:', formData);
|
||||
|
||||
// Reset form
|
||||
submitButton.classList.remove('loading');
|
||||
this.form.reset();
|
||||
this.validFields.clear();
|
||||
|
||||
// Reset all states
|
||||
Object.values(this.inputs).forEach(input => {
|
||||
input.group.classList.remove('valid', 'invalid');
|
||||
input.error.classList.remove('visible');
|
||||
input.isValid = false;
|
||||
});
|
||||
|
||||
this.updateProgress();
|
||||
this.updateStats();
|
||||
|
||||
// Show success message
|
||||
alert('Form submitted successfully!');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new InputIntelligence();
|
||||
});
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
/* Digital Minimalism Theme - Input Intelligence Styles */
|
||||
|
||||
:root {
|
||||
/* Monochromatic palette */
|
||||
--color-primary: #000000;
|
||||
--color-secondary: #666666;
|
||||
--color-tertiary: #999999;
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f8f8f8;
|
||||
--color-border: #e5e5e5;
|
||||
--color-error: #000000;
|
||||
--color-success: #000000;
|
||||
--color-warning: #666666;
|
||||
|
||||
/* Spacing system */
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 1rem;
|
||||
--space-md: 1.5rem;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 3rem;
|
||||
--space-xxl: 4rem;
|
||||
|
||||
/* Typography */
|
||||
--font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, Consolas, monospace;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.25rem;
|
||||
--font-size-xl: 2rem;
|
||||
|
||||
/* Animation */
|
||||
--transition-base: 0.2s ease;
|
||||
--transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: 1.6;
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-background);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-xxl);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Form Layout */
|
||||
.intelligent-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
/* Input Groups */
|
||||
.input-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--space-xs);
|
||||
color: var(--color-primary);
|
||||
transition: color var(--transition-base);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Intelligent Input */
|
||||
.intelligent-input {
|
||||
width: 100%;
|
||||
padding: var(--space-sm) 0;
|
||||
font-size: var(--font-size-base);
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
transition: border-color var(--transition-base);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.intelligent-input::placeholder {
|
||||
color: var(--color-tertiary);
|
||||
}
|
||||
|
||||
.intelligent-input:focus {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.intelligent-input.valid {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.intelligent-input.invalid {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Input Progress */
|
||||
.input-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-primary);
|
||||
transition: width var(--transition-slow);
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.input-group.active .input-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Input Indicator */
|
||||
.input-indicator {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-border);
|
||||
transition: all var(--transition-base);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.input-group.valid .input-indicator {
|
||||
background-color: var(--color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.input-group.invalid .input-indicator {
|
||||
background-color: var(--color-primary);
|
||||
opacity: 1;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.2); opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Hints and Errors */
|
||||
.input-hint {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-tertiary);
|
||||
margin-top: var(--space-xs);
|
||||
transition: opacity var(--transition-base);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-error);
|
||||
margin-top: var(--space-xs);
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
transition: all var(--transition-base);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.input-error.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* Suggestions */
|
||||
.suggestions-list {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--space-xs));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-8px);
|
||||
transition: all var(--transition-base);
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.suggestions-list.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: var(--space-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-base);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.suggestion-item:hover,
|
||||
.suggestion-item.selected {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
/* Card Type Indicator */
|
||||
.card-type {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-secondary);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-base);
|
||||
}
|
||||
|
||||
.card-type.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Password Toggle */
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: var(--space-xs);
|
||||
color: var(--color-secondary);
|
||||
transition: color var(--transition-base);
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.password-toggle-icon::before {
|
||||
content: '👁';
|
||||
font-size: var(--font-size-lg);
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.password-toggle.visible .password-toggle-icon::before {
|
||||
content: '👁🗨';
|
||||
}
|
||||
|
||||
/* Password Strength */
|
||||
.password-strength {
|
||||
margin-top: var(--space-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-base);
|
||||
}
|
||||
|
||||
.password-strength.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.strength-meter {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background-color: var(--color-border);
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.strength-fill {
|
||||
height: 100%;
|
||||
background-color: var(--color-primary);
|
||||
transition: width var(--transition-slow);
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.strength-text {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-secondary);
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Form Progress */
|
||||
.form-progress {
|
||||
margin-top: var(--space-lg);
|
||||
padding-top: var(--space-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 2px;
|
||||
background-color: var(--color-surface);
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--color-primary);
|
||||
transition: width var(--transition-slow);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Submit Button */
|
||||
.submit-button {
|
||||
margin-top: var(--space-lg);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.submit-button:not(:disabled):hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.submit-button:not(:disabled):active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--color-background);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.submit-button.loading .button-text {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.submit-button.loading .button-loader {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Footer Stats */
|
||||
.footer {
|
||||
margin-top: var(--space-xxl);
|
||||
padding-top: var(--space-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 300;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.intelligent-form {
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DATACOM-3000 // Retro Data Explorer</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="terminal-container">
|
||||
<header class="terminal-header">
|
||||
<div class="terminal-title">
|
||||
<span class="terminal-icon">◼◼◼</span>
|
||||
<h1>DATACOM-3000 SYSTEM v2.31</h1>
|
||||
</div>
|
||||
<div class="terminal-controls">
|
||||
<button class="terminal-btn minimize" aria-label="Minimize">_</button>
|
||||
<button class="terminal-btn maximize" aria-label="Maximize">□</button>
|
||||
<button class="terminal-btn close" aria-label="Close">X</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="command-bar">
|
||||
<div class="command-prompt">
|
||||
<span class="prompt-symbol">C:\DATA></span>
|
||||
<input type="text" class="command-input" placeholder="Enter command..." aria-label="Command input">
|
||||
</div>
|
||||
<div class="status-indicators">
|
||||
<span class="indicator" data-status="active">SYS</span>
|
||||
<span class="indicator" data-status="active">NET</span>
|
||||
<span class="indicator" data-status="idle">DSK</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="data-explorer">
|
||||
<section class="controls-panel">
|
||||
<div class="filter-group">
|
||||
<label for="search-filter" class="control-label">> SEARCH:</label>
|
||||
<input type="text" id="search-filter" class="filter-input" placeholder="*.*">
|
||||
</div>
|
||||
|
||||
<div class="sort-group">
|
||||
<label class="control-label">> SORT BY:</label>
|
||||
<select id="sort-selector" class="sort-select">
|
||||
<option value="id">ID</option>
|
||||
<option value="name">NAME</option>
|
||||
<option value="size">SIZE</option>
|
||||
<option value="date">DATE</option>
|
||||
</select>
|
||||
<button class="sort-direction" aria-label="Toggle sort direction">▲</button>
|
||||
</div>
|
||||
|
||||
<div class="action-group">
|
||||
<button class="action-btn" data-action="import">
|
||||
<span class="btn-icon">[↓]</span> IMPORT
|
||||
</button>
|
||||
<button class="action-btn" data-action="export">
|
||||
<span class="btn-icon">[↑]</span> EXPORT
|
||||
</button>
|
||||
<button class="action-btn" data-action="refresh">
|
||||
<span class="btn-icon">[↻]</span> REFRESH
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="data-viewport">
|
||||
<div class="data-table-wrapper">
|
||||
<table class="data-table" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" class="column-header" data-column="id">
|
||||
<span class="header-text">ID</span>
|
||||
<span class="resize-handle" aria-hidden="true"></span>
|
||||
</th>
|
||||
<th role="columnheader" class="column-header" data-column="name">
|
||||
<span class="header-text">NAME</span>
|
||||
<span class="resize-handle" aria-hidden="true"></span>
|
||||
</th>
|
||||
<th role="columnheader" class="column-header" data-column="type">
|
||||
<span class="header-text">TYPE</span>
|
||||
<span class="resize-handle" aria-hidden="true"></span>
|
||||
</th>
|
||||
<th role="columnheader" class="column-header" data-column="size">
|
||||
<span class="header-text">SIZE</span>
|
||||
<span class="resize-handle" aria-hidden="true"></span>
|
||||
</th>
|
||||
<th role="columnheader" class="column-header" data-column="modified">
|
||||
<span class="header-text">MODIFIED</span>
|
||||
<span class="resize-handle" aria-hidden="true"></span>
|
||||
</th>
|
||||
<th role="columnheader" class="column-header" data-column="preview">
|
||||
<span class="header-text">PREVIEW</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="data-tbody" role="rowgroup">
|
||||
<!-- Data rows will be dynamically inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<aside class="data-preview">
|
||||
<div class="preview-header">
|
||||
<h3>> DATA PREVIEW</h3>
|
||||
<button class="preview-close" aria-label="Close preview">X</button>
|
||||
</div>
|
||||
<div class="preview-content" id="preview-content">
|
||||
<pre class="ascii-art">
|
||||
╔═══════════════════╗
|
||||
║ NO DATA SELECTED ║
|
||||
║ ║
|
||||
║ SELECT A ROW TO ║
|
||||
║ VIEW PREVIEW ║
|
||||
╚═══════════════════╝
|
||||
</pre>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<footer class="status-bar">
|
||||
<div class="memory-usage">
|
||||
<span class="label">MEM:</span>
|
||||
<div class="memory-bar">
|
||||
<div class="memory-used"></div>
|
||||
</div>
|
||||
<span class="memory-text">640K/1024K</span>
|
||||
</div>
|
||||
<div class="record-count">
|
||||
<span class="label">RECORDS:</span>
|
||||
<span id="record-count">0</span>
|
||||
</div>
|
||||
<div class="system-time">
|
||||
<span id="system-time">00:00:00</span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<div class="scanlines" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
// DATACOM-3000 Data Explorer JavaScript Module
|
||||
|
||||
class DataExplorer {
|
||||
constructor() {
|
||||
this.data = [];
|
||||
this.filteredData = [];
|
||||
this.sortColumn = 'id';
|
||||
this.sortDirection = 'asc';
|
||||
this.selectedRow = null;
|
||||
this.columns = ['id', 'name', 'type', 'size', 'modified'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.generateMockData();
|
||||
this.updateDisplay();
|
||||
this.startSystemClock();
|
||||
this.simulateMemoryUsage();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Search filter
|
||||
const searchInput = document.getElementById('search-filter');
|
||||
searchInput.addEventListener('input', (e) => this.handleSearch(e.target.value));
|
||||
|
||||
// Sort controls
|
||||
const sortSelector = document.getElementById('sort-selector');
|
||||
sortSelector.addEventListener('change', (e) => this.handleSort(e.target.value));
|
||||
|
||||
const sortDirection = document.querySelector('.sort-direction');
|
||||
sortDirection.addEventListener('click', () => this.toggleSortDirection());
|
||||
|
||||
// Action buttons
|
||||
document.querySelectorAll('.action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const action = e.currentTarget.dataset.action;
|
||||
this.handleAction(action);
|
||||
});
|
||||
});
|
||||
|
||||
// Column headers for sorting
|
||||
document.querySelectorAll('.column-header').forEach(header => {
|
||||
header.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('resize-handle')) {
|
||||
const column = header.dataset.column;
|
||||
this.handleSort(column);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Column resizing
|
||||
this.initColumnResizing();
|
||||
|
||||
// Preview close button
|
||||
const previewClose = document.querySelector('.preview-close');
|
||||
previewClose.addEventListener('click', () => this.closePreview());
|
||||
|
||||
// Command input
|
||||
const commandInput = document.querySelector('.command-input');
|
||||
commandInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.executeCommand(e.target.value);
|
||||
e.target.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateMockData() {
|
||||
const fileTypes = ['DAT', 'TXT', 'BIN', 'LOG', 'CFG', 'SYS', 'EXE', 'BAT'];
|
||||
const names = [
|
||||
'SYSTEM', 'CONFIG', 'DATA', 'BACKUP', 'ARCHIVE', 'TEMP',
|
||||
'CACHE', 'INDEX', 'DATABASE', 'REPORT', 'LOG', 'USER'
|
||||
];
|
||||
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
const type = fileTypes[Math.floor(Math.random() * fileTypes.length)];
|
||||
const name = names[Math.floor(Math.random() * names.length)];
|
||||
const size = Math.floor(Math.random() * 999999) + 1;
|
||||
const date = new Date(Date.now() - Math.floor(Math.random() * 31536000000));
|
||||
|
||||
this.data.push({
|
||||
id: String(i).padStart(4, '0'),
|
||||
name: `${name}_${String(i).padStart(2, '0')}.${type}`,
|
||||
type: type,
|
||||
size: this.formatSize(size),
|
||||
sizeBytes: size,
|
||||
modified: this.formatDate(date),
|
||||
modifiedTimestamp: date.getTime(),
|
||||
rawData: this.generatePreviewData(type)
|
||||
});
|
||||
}
|
||||
|
||||
this.filteredData = [...this.data];
|
||||
}
|
||||
|
||||
generatePreviewData(type) {
|
||||
const previews = {
|
||||
'DAT': '00000000 48 45 58 20 44 55 4D 50 20 44 41 54 41 20 46 49 |HEX DUMP DATA FI|\n00000010 4C 45 20 46 4F 52 20 50 52 45 56 49 45 57 20 4F |LE FOR PREVIEW O|\n00000020 4E 4C 59 20 2D 20 4E 4F 54 20 41 43 54 55 41 4C |NLY - NOT ACTUAL|',
|
||||
'TXT': 'PLAINTEXT DOCUMENT\n==================\nLorem ipsum dolor sit amet, consectetur adipiscing elit.\nSed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n[END OF FILE]',
|
||||
'BIN': '01001000 01000101 01001100 01001100 01001111\n01010111 01001111 01010010 01001100 01000100\n00100001 00100000 00110010 00110000 00110010\n00110100 00100000 01000010 01001001 01001110',
|
||||
'LOG': '[1990-01-01 00:00:00] SYSTEM INITIALIZED\n[1990-01-01 00:00:01] MEMORY CHECK... OK\n[1990-01-01 00:00:02] DISK CHECK... OK\n[1990-01-01 00:00:03] NETWORK CHECK... OK\n[1990-01-01 00:00:04] READY FOR OPERATION',
|
||||
'CFG': 'SYSTEM.MEMORY=640K\nSYSTEM.DISK=40MB\nSYSTEM.CPU=80486\nSYSTEM.SPEED=33MHZ\nSYSTEM.CACHE=ENABLED\nSYSTEM.SOUND=BEEPER',
|
||||
'SYS': 'SYSTEM FILE - PROTECTED\n\nACCESS DENIED\n\nAUTHORIZATION REQUIRED',
|
||||
'EXE': 'EXECUTABLE FILE HEADER\n=====================\nMAGIC NUMBER: 4D5A\nBYTES ON LAST PAGE: 0090\nPAGES IN FILE: 0003\nRELOCATIONS: 0000\nHEADER SIZE: 0004',
|
||||
'BAT': '@ECHO OFF\nCLS\nECHO BATCH FILE PREVIEW\nECHO ==================\nPAUSE\nEXIT'
|
||||
};
|
||||
|
||||
return previews[type] || 'UNKNOWN FILE TYPE';
|
||||
}
|
||||
|
||||
formatSize(bytes) {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1048576) return `${Math.floor(bytes / 1024)}K`;
|
||||
return `${(bytes / 1048576).toFixed(1)}M`;
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
handleSearch(query) {
|
||||
if (!query) {
|
||||
this.filteredData = [...this.data];
|
||||
} else {
|
||||
const searchTerm = query.toLowerCase();
|
||||
this.filteredData = this.data.filter(item =>
|
||||
item.name.toLowerCase().includes(searchTerm) ||
|
||||
item.type.toLowerCase().includes(searchTerm) ||
|
||||
item.id.includes(searchTerm)
|
||||
);
|
||||
}
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
handleSort(column) {
|
||||
if (this.sortColumn === column) {
|
||||
this.toggleSortDirection();
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = 'asc';
|
||||
document.querySelector('.sort-direction').textContent = '▲';
|
||||
}
|
||||
|
||||
this.sortData();
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
toggleSortDirection() {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
const directionBtn = document.querySelector('.sort-direction');
|
||||
directionBtn.textContent = this.sortDirection === 'asc' ? '▲' : '▼';
|
||||
this.sortData();
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
sortData() {
|
||||
this.filteredData.sort((a, b) => {
|
||||
let valueA, valueB;
|
||||
|
||||
switch (this.sortColumn) {
|
||||
case 'size':
|
||||
valueA = a.sizeBytes;
|
||||
valueB = b.sizeBytes;
|
||||
break;
|
||||
case 'date':
|
||||
case 'modified':
|
||||
valueA = a.modifiedTimestamp;
|
||||
valueB = b.modifiedTimestamp;
|
||||
break;
|
||||
default:
|
||||
valueA = a[this.sortColumn];
|
||||
valueB = b[this.sortColumn];
|
||||
}
|
||||
|
||||
if (valueA < valueB) return this.sortDirection === 'asc' ? -1 : 1;
|
||||
if (valueA > valueB) return this.sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
handleAction(action) {
|
||||
switch (action) {
|
||||
case 'import':
|
||||
this.showMessage('IMPORT FUNCTION NOT IMPLEMENTED');
|
||||
this.simulateImport();
|
||||
break;
|
||||
case 'export':
|
||||
this.exportData();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
simulateImport() {
|
||||
// Simulate file import
|
||||
setTimeout(() => {
|
||||
const newItem = {
|
||||
id: String(this.data.length + 1).padStart(4, '0'),
|
||||
name: `IMPORT_${Date.now()}.DAT`,
|
||||
type: 'DAT',
|
||||
size: this.formatSize(Math.floor(Math.random() * 99999) + 1000),
|
||||
sizeBytes: Math.floor(Math.random() * 99999) + 1000,
|
||||
modified: this.formatDate(new Date()),
|
||||
modifiedTimestamp: Date.now(),
|
||||
rawData: 'IMPORTED DATA FILE\n================\n[DATA IMPORTED SUCCESSFULLY]'
|
||||
};
|
||||
|
||||
this.data.push(newItem);
|
||||
this.filteredData = [...this.data];
|
||||
this.updateDisplay();
|
||||
this.showMessage('IMPORT COMPLETE: 1 FILE ADDED');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
exportData() {
|
||||
const exportText = this.filteredData.map(item =>
|
||||
`${item.id},${item.name},${item.type},${item.size},${item.modified}`
|
||||
).join('\n');
|
||||
|
||||
const header = 'ID,NAME,TYPE,SIZE,MODIFIED\n';
|
||||
const fullExport = header + exportText;
|
||||
|
||||
// Create download
|
||||
const blob = new Blob([fullExport], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `DATACOM_EXPORT_${Date.now()}.CSV`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showMessage(`EXPORT COMPLETE: ${this.filteredData.length} RECORDS`);
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
// Simulate refresh animation
|
||||
const tbody = document.getElementById('data-tbody');
|
||||
tbody.style.opacity = '0.5';
|
||||
|
||||
setTimeout(() => {
|
||||
tbody.style.opacity = '1';
|
||||
this.updateDisplay();
|
||||
this.showMessage('DATA REFRESHED');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
const tbody = document.getElementById('data-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
this.filteredData.forEach((item, index) => {
|
||||
const row = document.createElement('tr');
|
||||
row.setAttribute('role', 'row');
|
||||
row.dataset.index = index;
|
||||
|
||||
row.innerHTML = `
|
||||
<td role="gridcell">${item.id}</td>
|
||||
<td role="gridcell">${item.name}</td>
|
||||
<td role="gridcell">${item.type}</td>
|
||||
<td role="gridcell">${item.size}</td>
|
||||
<td role="gridcell">${item.modified}</td>
|
||||
<td role="gridcell">
|
||||
<button class="preview-btn" data-index="${index}">▶</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
row.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('preview-btn')) {
|
||||
this.selectRow(row, item);
|
||||
}
|
||||
});
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Bind preview buttons
|
||||
document.querySelectorAll('.preview-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const index = parseInt(btn.dataset.index);
|
||||
this.showPreview(this.filteredData[index]);
|
||||
});
|
||||
});
|
||||
|
||||
// Update record count
|
||||
document.getElementById('record-count').textContent = this.filteredData.length;
|
||||
}
|
||||
|
||||
selectRow(row, item) {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.data-table tbody tr').forEach(tr => {
|
||||
tr.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selection to current row
|
||||
row.classList.add('selected');
|
||||
this.selectedRow = item;
|
||||
}
|
||||
|
||||
showPreview(item) {
|
||||
const previewContent = document.getElementById('preview-content');
|
||||
previewContent.innerHTML = `
|
||||
<div class="preview-info">
|
||||
<p><span class="label">FILE:</span> ${item.name}</p>
|
||||
<p><span class="label">TYPE:</span> ${item.type}</p>
|
||||
<p><span class="label">SIZE:</span> ${item.size}</p>
|
||||
<p><span class="label">DATE:</span> ${item.modified}</p>
|
||||
</div>
|
||||
<div class="preview-separator">════════════════════</div>
|
||||
<pre class="preview-data">${item.rawData}</pre>
|
||||
`;
|
||||
|
||||
// Show preview panel on mobile
|
||||
const preview = document.querySelector('.data-preview');
|
||||
preview.classList.add('active');
|
||||
}
|
||||
|
||||
closePreview() {
|
||||
const preview = document.querySelector('.data-preview');
|
||||
preview.classList.remove('active');
|
||||
}
|
||||
|
||||
executeCommand(command) {
|
||||
const cmd = command.toUpperCase().trim();
|
||||
|
||||
switch (cmd) {
|
||||
case 'CLS':
|
||||
case 'CLEAR':
|
||||
this.filteredData = [];
|
||||
this.updateDisplay();
|
||||
this.showMessage('SCREEN CLEARED');
|
||||
break;
|
||||
case 'DIR':
|
||||
case 'LS':
|
||||
this.showMessage(`${this.data.length} FILES IN DIRECTORY`);
|
||||
break;
|
||||
case 'HELP':
|
||||
this.showMessage('COMMANDS: CLS, DIR, SORT, FILTER, EXIT');
|
||||
break;
|
||||
case 'EXIT':
|
||||
this.showMessage('CANNOT EXIT - SYSTEM LOCKED');
|
||||
break;
|
||||
default:
|
||||
if (cmd.startsWith('FILTER ')) {
|
||||
const query = cmd.substring(7);
|
||||
document.getElementById('search-filter').value = query;
|
||||
this.handleSearch(query);
|
||||
} else if (cmd.startsWith('SORT ')) {
|
||||
const column = cmd.substring(5).toLowerCase();
|
||||
if (this.columns.includes(column)) {
|
||||
this.handleSort(column);
|
||||
}
|
||||
} else {
|
||||
this.showMessage('UNKNOWN COMMAND - TYPE HELP');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(message) {
|
||||
const commandInput = document.querySelector('.command-input');
|
||||
const originalPlaceholder = commandInput.placeholder;
|
||||
commandInput.placeholder = message;
|
||||
|
||||
setTimeout(() => {
|
||||
commandInput.placeholder = originalPlaceholder;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
initColumnResizing() {
|
||||
let isResizing = false;
|
||||
let currentColumn = null;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
|
||||
document.querySelectorAll('.resize-handle').forEach(handle => {
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
currentColumn = handle.parentElement;
|
||||
startX = e.pageX;
|
||||
startWidth = currentColumn.offsetWidth;
|
||||
|
||||
document.body.style.cursor = 'col-resize';
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const width = startWidth + (e.pageX - startX);
|
||||
if (width > 50) {
|
||||
currentColumn.style.width = `${width}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isResizing = false;
|
||||
currentColumn = null;
|
||||
document.body.style.cursor = 'default';
|
||||
});
|
||||
}
|
||||
|
||||
startSystemClock() {
|
||||
const updateClock = () => {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
document.getElementById('system-time').textContent = `${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
updateClock();
|
||||
setInterval(updateClock, 1000);
|
||||
}
|
||||
|
||||
simulateMemoryUsage() {
|
||||
const memoryBar = document.querySelector('.memory-used');
|
||||
const memoryText = document.querySelector('.memory-text');
|
||||
|
||||
setInterval(() => {
|
||||
const used = 640 + Math.floor(Math.random() * 100);
|
||||
const percentage = (used / 1024) * 100;
|
||||
|
||||
memoryBar.style.width = `${percentage}%`;
|
||||
memoryText.textContent = `${used}K/1024K`;
|
||||
|
||||
if (percentage > 90) {
|
||||
memoryBar.style.backgroundColor = 'var(--error-red)';
|
||||
} else if (percentage > 75) {
|
||||
memoryBar.style.backgroundColor = 'var(--warning-yellow)';
|
||||
} else {
|
||||
memoryBar.style.backgroundColor = 'var(--terminal-text)';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.dataExplorer = new DataExplorer();
|
||||
|
||||
// Add keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
switch (e.key) {
|
||||
case 'f':
|
||||
e.preventDefault();
|
||||
document.getElementById('search-filter').focus();
|
||||
break;
|
||||
case 'e':
|
||||
e.preventDefault();
|
||||
window.dataExplorer.handleAction('export');
|
||||
break;
|
||||
case 'r':
|
||||
e.preventDefault();
|
||||
window.dataExplorer.handleAction('refresh');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add some retro console messages
|
||||
console.log('%c╔════════════════════════════╗', 'color: #00ff00');
|
||||
console.log('%c║ DATACOM-3000 SYSTEM v2.31 ║', 'color: #00ff00');
|
||||
console.log('%c║ (C) 1990 RETROTECH CORP ║', 'color: #00ff00');
|
||||
console.log('%c╚════════════════════════════╝', 'color: #00ff00');
|
||||
console.log('%cSYSTEM INITIALIZED', 'color: #ffb000');
|
||||
console.log('%cMEMORY: 640K OK', 'color: #00ff00');
|
||||
console.log('%cREADY.', 'color: #00ff00');
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
/* DATACOM-3000 Retro Computing Theme */
|
||||
:root {
|
||||
/* Terminal Colors */
|
||||
--terminal-green: #00ff00;
|
||||
--terminal-amber: #ffb000;
|
||||
--terminal-bg: #0a0a0a;
|
||||
--terminal-bg-light: #1a1a1a;
|
||||
--terminal-border: #333333;
|
||||
--terminal-glow: rgba(0, 255, 0, 0.5);
|
||||
--terminal-text: #00ff00;
|
||||
--terminal-dim: #00aa00;
|
||||
|
||||
/* System Colors */
|
||||
--error-red: #ff0040;
|
||||
--warning-yellow: #ffff00;
|
||||
--info-cyan: #00ffff;
|
||||
|
||||
/* Typography */
|
||||
--font-mono: 'Courier New', Courier, monospace;
|
||||
--font-size-base: 14px;
|
||||
--line-height: 1.4;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
|
||||
/* Effects */
|
||||
--scanline-opacity: 0.05;
|
||||
--crt-curve: 0.02;
|
||||
}
|
||||
|
||||
/* Global Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height);
|
||||
background-color: #000;
|
||||
color: var(--terminal-text);
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* CRT Effect Container */
|
||||
.terminal-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--terminal-bg);
|
||||
border: 2px solid var(--terminal-border);
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(0, 255, 0, 0.1),
|
||||
0 0 40px var(--terminal-glow);
|
||||
}
|
||||
|
||||
/* Terminal Header */
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: var(--terminal-bg-light);
|
||||
border-bottom: 2px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.terminal-icon {
|
||||
color: var(--terminal-amber);
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.terminal-title h1 {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.terminal-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--terminal-text);
|
||||
color: var(--terminal-text);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.terminal-btn:hover {
|
||||
background-color: var(--terminal-text);
|
||||
color: var(--terminal-bg);
|
||||
box-shadow: 0 0 10px var(--terminal-glow);
|
||||
}
|
||||
|
||||
/* Command Bar */
|
||||
.command-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: var(--terminal-bg-light);
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.command-prompt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.prompt-symbol {
|
||||
color: var(--terminal-amber);
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.command-input {
|
||||
flex: 1;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--terminal-text);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.command-input::placeholder {
|
||||
color: var(--terminal-dim);
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--terminal-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.indicator[data-status="active"] {
|
||||
background-color: var(--terminal-text);
|
||||
color: var(--terminal-bg);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.indicator[data-status="idle"] {
|
||||
color: var(--terminal-dim);
|
||||
border-color: var(--terminal-dim);
|
||||
}
|
||||
|
||||
/* Data Explorer Main Area */
|
||||
.data-explorer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
/* Controls Panel */
|
||||
.controls-panel {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background-color: var(--terminal-bg-light);
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.filter-group,
|
||||
.sort-group,
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.control-label {
|
||||
color: var(--terminal-amber);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-input,
|
||||
.sort-select {
|
||||
background-color: var(--terminal-bg);
|
||||
border: 1px solid var(--terminal-border);
|
||||
color: var(--terminal-text);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filter-input:focus,
|
||||
.sort-select:focus {
|
||||
border-color: var(--terminal-text);
|
||||
box-shadow: 0 0 5px var(--terminal-glow);
|
||||
}
|
||||
|
||||
.sort-direction {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--terminal-border);
|
||||
color: var(--terminal-text);
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background-color: var(--terminal-bg);
|
||||
border: 1px solid var(--terminal-border);
|
||||
color: var(--terminal-text);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--terminal-text);
|
||||
box-shadow: 0 0 10px var(--terminal-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
color: var(--terminal-amber);
|
||||
}
|
||||
|
||||
/* Data Viewport */
|
||||
.data-viewport {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Data Table */
|
||||
.data-table-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background-color: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
position: relative;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: var(--terminal-bg-light);
|
||||
border: 1px solid var(--terminal-border);
|
||||
color: var(--terminal-amber);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.column-header:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background-color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background-color: var(--terminal-bg-light);
|
||||
}
|
||||
|
||||
.data-table tbody tr.selected {
|
||||
background-color: rgba(0, 255, 0, 0.1);
|
||||
box-shadow: inset 0 0 20px var(--terminal-glow);
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
color: var(--terminal-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Data Preview */
|
||||
.data-preview {
|
||||
width: 300px;
|
||||
background-color: var(--terminal-bg-light);
|
||||
border-left: 2px solid var(--terminal-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.preview-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: var(--terminal-amber);
|
||||
}
|
||||
|
||||
.preview-close {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--terminal-text);
|
||||
color: var(--terminal-text);
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
padding: var(--spacing-md);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ascii-art {
|
||||
color: var(--terminal-dim);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: var(--terminal-bg-light);
|
||||
border-top: 2px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.memory-usage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.memory-bar {
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
background-color: var(--terminal-bg);
|
||||
border: 1px solid var(--terminal-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.memory-used {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 62.5%;
|
||||
background-color: var(--terminal-text);
|
||||
animation: memory-pulse 3s infinite;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--terminal-amber);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-text,
|
||||
#record-count,
|
||||
#system-time {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* CRT Scanlines Effect */
|
||||
.scanlines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
transparent 50%,
|
||||
rgba(0, 255, 0, var(--scanline-opacity)) 50%
|
||||
);
|
||||
background-size: 100% 4px;
|
||||
animation: scanlines 8s linear infinite;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes memory-pulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scanlines {
|
||||
0% { transform: translateY(0); }
|
||||
100% { transform: translateY(4px); }
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--terminal-bg);
|
||||
border: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--terminal-dim);
|
||||
border: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--terminal-text);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.data-preview {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.data-preview.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls-panel {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility Focus Styles */
|
||||
*:focus {
|
||||
outline: 2px solid var(--terminal-amber);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.terminal-container {
|
||||
box-shadow: none;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.scanlines {
|
||||
display: none;
|
||||
}
|
||||
|
||||
* {
|
||||
color: #000 !important;
|
||||
background-color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neon Wave Player - Cyberpunk Media Interface</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="media-player" role="application" aria-label="Neon Wave Media Player">
|
||||
<div class="player-container">
|
||||
<!-- Visualizer Section -->
|
||||
<section class="visualizer-section" aria-label="Audio Visualizer">
|
||||
<canvas id="waveform-visualizer" class="waveform-canvas" width="800" height="200"></canvas>
|
||||
<div class="glitch-overlay" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<!-- Media Display -->
|
||||
<section class="media-display" aria-label="Now Playing">
|
||||
<div class="album-art-container">
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Crect width='300' height='300' fill='%23111'/%3E%3Ctext x='150' y='150' text-anchor='middle' fill='%23ff00ff' font-size='24'%3ENO MEDIA%3C/text%3E%3C/svg%3E"
|
||||
alt="Album artwork"
|
||||
class="album-art"
|
||||
id="album-art">
|
||||
<div class="hologram-effect" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="track-info">
|
||||
<h1 id="track-title" class="track-title">Select a Track</h1>
|
||||
<p id="track-artist" class="track-artist">No Artist</p>
|
||||
<div class="track-metadata">
|
||||
<span id="track-genre" class="metadata-tag">Genre</span>
|
||||
<span id="track-year" class="metadata-tag">Year</span>
|
||||
<span id="track-bpm" class="metadata-tag">BPM</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<section class="progress-section" aria-label="Playback Progress">
|
||||
<div class="time-display">
|
||||
<time id="current-time" class="time-current">00:00</time>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
<div class="progress-glow"></div>
|
||||
</div>
|
||||
<input type="range"
|
||||
id="seek-slider"
|
||||
class="seek-slider"
|
||||
min="0"
|
||||
max="100"
|
||||
value="0"
|
||||
aria-label="Seek position">
|
||||
</div>
|
||||
<time id="duration-time" class="time-duration">00:00</time>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<section class="control-panel" aria-label="Playback Controls">
|
||||
<div class="main-controls">
|
||||
<button class="control-btn" id="shuffle-btn" aria-label="Shuffle" data-active="false">
|
||||
<svg viewBox="0 0 24 24" class="control-icon">
|
||||
<path d="M3 17h2.5L12 7h-2.5l-6.5 10zm6.5-10L3 17h2.5l6.5-10H9.5zm5 0L21 17h-2.5L12 7h2.5zm-2.5 10h2.5l6.5-10H19l-6.5 10z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="control-btn" id="prev-btn" aria-label="Previous track">
|
||||
<svg viewBox="0 0 24 24" class="control-icon">
|
||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="control-btn control-btn-primary" id="play-pause-btn" aria-label="Play/Pause">
|
||||
<svg viewBox="0 0 24 24" class="control-icon" id="play-icon">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" class="control-icon hidden" id="pause-icon">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="control-btn" id="next-btn" aria-label="Next track">
|
||||
<svg viewBox="0 0 24 24" class="control-icon">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="control-btn" id="repeat-btn" aria-label="Repeat" data-mode="off">
|
||||
<svg viewBox="0 0 24 24" class="control-icon">
|
||||
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Volume and EQ Section -->
|
||||
<section class="audio-controls" aria-label="Audio Controls">
|
||||
<div class="volume-control">
|
||||
<button class="control-btn" id="volume-btn" aria-label="Volume">
|
||||
<svg viewBox="0 0 24 24" class="control-icon">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="volume-slider-container">
|
||||
<input type="range"
|
||||
id="volume-slider"
|
||||
class="volume-slider"
|
||||
min="0"
|
||||
max="100"
|
||||
value="70"
|
||||
aria-label="Volume level">
|
||||
<div class="volume-level" id="volume-level">70%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equalizer">
|
||||
<button class="eq-preset" data-preset="flat">FLAT</button>
|
||||
<button class="eq-preset" data-preset="bass">BASS</button>
|
||||
<button class="eq-preset" data-preset="vocal">VOCAL</button>
|
||||
<button class="eq-preset active" data-preset="cyber">CYBER</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Playlist Section -->
|
||||
<section class="playlist-section" aria-label="Playlist">
|
||||
<header class="playlist-header">
|
||||
<h2 class="playlist-title">Neural Tracks</h2>
|
||||
<button class="playlist-action" id="add-track-btn" aria-label="Add track">
|
||||
<svg viewBox="0 0 24 24" class="action-icon">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="playlist-container" id="playlist-container">
|
||||
<ul class="playlist" id="playlist" role="list" aria-label="Track list">
|
||||
<!-- Playlist items will be dynamically added -->
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Audio Element -->
|
||||
<audio id="audio-player" class="hidden"></audio>
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,448 @@
|
|||
// Neon Wave Player - JavaScript Module
|
||||
|
||||
class NeonWavePlayer {
|
||||
constructor() {
|
||||
// Audio elements
|
||||
this.audio = document.getElementById('audio-player');
|
||||
this.canvas = document.getElementById('waveform-visualizer');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// Audio context for visualizer
|
||||
this.audioContext = null;
|
||||
this.analyser = null;
|
||||
this.source = null;
|
||||
this.dataArray = null;
|
||||
|
||||
// Player state
|
||||
this.isPlaying = false;
|
||||
this.currentTrackIndex = 0;
|
||||
this.volume = 0.7;
|
||||
this.shuffleMode = false;
|
||||
this.repeatMode = 'off'; // off, one, all
|
||||
|
||||
// Playlist data
|
||||
this.playlist = [
|
||||
{
|
||||
title: 'Digital Dreams',
|
||||
artist: 'Neon Collective',
|
||||
duration: '3:45',
|
||||
genre: 'Synthwave',
|
||||
year: '2089',
|
||||
bpm: '128',
|
||||
url: 'https://example.com/track1.mp3'
|
||||
},
|
||||
{
|
||||
title: 'Chrome Hearts',
|
||||
artist: 'Cyber Punk',
|
||||
duration: '4:12',
|
||||
genre: 'Dark Synth',
|
||||
year: '2090',
|
||||
bpm: '140',
|
||||
url: 'https://example.com/track2.mp3'
|
||||
},
|
||||
{
|
||||
title: 'Neural Network',
|
||||
artist: 'AI Composer',
|
||||
duration: '5:23',
|
||||
genre: 'Ambient',
|
||||
year: '2091',
|
||||
bpm: '90',
|
||||
url: 'https://example.com/track3.mp3'
|
||||
},
|
||||
{
|
||||
title: 'Hologram Highway',
|
||||
artist: 'Future Bass',
|
||||
duration: '3:58',
|
||||
genre: 'Electronic',
|
||||
year: '2088',
|
||||
bpm: '150',
|
||||
url: 'https://example.com/track4.mp3'
|
||||
},
|
||||
{
|
||||
title: 'Quantum Flux',
|
||||
artist: 'Void Walker',
|
||||
duration: '6:30',
|
||||
genre: 'Experimental',
|
||||
year: '2092',
|
||||
bpm: '175',
|
||||
url: 'https://example.com/track5.mp3'
|
||||
}
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.renderPlaylist();
|
||||
this.loadTrack(0);
|
||||
this.setupAudioContext();
|
||||
this.resizeCanvas();
|
||||
|
||||
// Start animation loop
|
||||
this.animate();
|
||||
}
|
||||
|
||||
setupAudioContext() {
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
this.analyser.smoothingTimeConstant = 0.8;
|
||||
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
// Connect audio element to analyser
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
} catch (error) {
|
||||
console.error('Failed to setup audio context:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Play/Pause
|
||||
document.getElementById('play-pause-btn').addEventListener('click', () => this.togglePlayPause());
|
||||
|
||||
// Navigation
|
||||
document.getElementById('prev-btn').addEventListener('click', () => this.previousTrack());
|
||||
document.getElementById('next-btn').addEventListener('click', () => this.nextTrack());
|
||||
|
||||
// Modes
|
||||
document.getElementById('shuffle-btn').addEventListener('click', () => this.toggleShuffle());
|
||||
document.getElementById('repeat-btn').addEventListener('click', () => this.toggleRepeat());
|
||||
|
||||
// Volume
|
||||
const volumeSlider = document.getElementById('volume-slider');
|
||||
volumeSlider.addEventListener('input', (e) => this.setVolume(e.target.value / 100));
|
||||
|
||||
// Seek
|
||||
const seekSlider = document.getElementById('seek-slider');
|
||||
seekSlider.addEventListener('input', (e) => this.seek(e.target.value));
|
||||
|
||||
// Audio events
|
||||
this.audio.addEventListener('timeupdate', () => this.updateProgress());
|
||||
this.audio.addEventListener('ended', () => this.handleTrackEnd());
|
||||
this.audio.addEventListener('loadedmetadata', () => this.updateDuration());
|
||||
|
||||
// EQ presets
|
||||
document.querySelectorAll('.eq-preset').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => this.setEQPreset(e.target.dataset.preset));
|
||||
});
|
||||
|
||||
// Add track button
|
||||
document.getElementById('add-track-btn').addEventListener('click', () => this.addTrack());
|
||||
|
||||
// Window resize
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
const container = this.canvas.parentElement;
|
||||
this.canvas.width = container.offsetWidth;
|
||||
this.canvas.height = 200;
|
||||
}
|
||||
|
||||
loadTrack(index) {
|
||||
const track = this.playlist[index];
|
||||
if (!track) return;
|
||||
|
||||
this.currentTrackIndex = index;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('track-title').textContent = track.title;
|
||||
document.getElementById('track-artist').textContent = track.artist;
|
||||
document.getElementById('track-genre').textContent = track.genre;
|
||||
document.getElementById('track-year').textContent = track.year;
|
||||
document.getElementById('track-bpm').textContent = track.bpm + ' BPM';
|
||||
|
||||
// Update playlist highlighting
|
||||
this.updatePlaylistHighlight();
|
||||
|
||||
// Load audio (using placeholder URL)
|
||||
// In real implementation, this would load actual audio files
|
||||
// this.audio.src = track.url;
|
||||
|
||||
// Reset progress
|
||||
document.getElementById('progress-fill').style.width = '0%';
|
||||
document.getElementById('current-time').textContent = '00:00';
|
||||
document.getElementById('duration-time').textContent = track.duration;
|
||||
}
|
||||
|
||||
togglePlayPause() {
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
// In real implementation, this would play the audio
|
||||
// this.audio.play();
|
||||
this.isPlaying = true;
|
||||
document.getElementById('play-icon').classList.add('hidden');
|
||||
document.getElementById('pause-icon').classList.remove('hidden');
|
||||
|
||||
// Simulate playback
|
||||
this.simulatePlayback();
|
||||
}
|
||||
|
||||
pause() {
|
||||
// In real implementation, this would pause the audio
|
||||
// this.audio.pause();
|
||||
this.isPlaying = false;
|
||||
document.getElementById('play-icon').classList.remove('hidden');
|
||||
document.getElementById('pause-icon').classList.add('hidden');
|
||||
|
||||
// Stop simulation
|
||||
if (this.playbackInterval) {
|
||||
clearInterval(this.playbackInterval);
|
||||
}
|
||||
}
|
||||
|
||||
simulatePlayback() {
|
||||
let progress = 0;
|
||||
this.playbackInterval = setInterval(() => {
|
||||
if (progress >= 100) {
|
||||
this.handleTrackEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
progress += 0.5;
|
||||
document.getElementById('progress-fill').style.width = progress + '%';
|
||||
document.getElementById('seek-slider').value = progress;
|
||||
|
||||
// Update time display
|
||||
const duration = this.parseDuration(this.playlist[this.currentTrackIndex].duration);
|
||||
const current = (progress / 100) * duration;
|
||||
document.getElementById('current-time').textContent = this.formatTime(current);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
parseDuration(durationStr) {
|
||||
const [minutes, seconds] = durationStr.split(':').map(Number);
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
previousTrack() {
|
||||
if (this.shuffleMode) {
|
||||
this.playRandomTrack();
|
||||
} else {
|
||||
this.currentTrackIndex = (this.currentTrackIndex - 1 + this.playlist.length) % this.playlist.length;
|
||||
this.loadTrack(this.currentTrackIndex);
|
||||
if (this.isPlaying) this.play();
|
||||
}
|
||||
}
|
||||
|
||||
nextTrack() {
|
||||
if (this.shuffleMode) {
|
||||
this.playRandomTrack();
|
||||
} else {
|
||||
this.currentTrackIndex = (this.currentTrackIndex + 1) % this.playlist.length;
|
||||
this.loadTrack(this.currentTrackIndex);
|
||||
if (this.isPlaying) this.play();
|
||||
}
|
||||
}
|
||||
|
||||
playRandomTrack() {
|
||||
let newIndex;
|
||||
do {
|
||||
newIndex = Math.floor(Math.random() * this.playlist.length);
|
||||
} while (newIndex === this.currentTrackIndex && this.playlist.length > 1);
|
||||
|
||||
this.loadTrack(newIndex);
|
||||
if (this.isPlaying) this.play();
|
||||
}
|
||||
|
||||
handleTrackEnd() {
|
||||
if (this.repeatMode === 'one') {
|
||||
this.seek(0);
|
||||
this.play();
|
||||
} else if (this.repeatMode === 'all' || this.currentTrackIndex < this.playlist.length - 1) {
|
||||
this.nextTrack();
|
||||
} else {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
toggleShuffle() {
|
||||
this.shuffleMode = !this.shuffleMode;
|
||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||
shuffleBtn.dataset.active = this.shuffleMode;
|
||||
}
|
||||
|
||||
toggleRepeat() {
|
||||
const modes = ['off', 'all', 'one'];
|
||||
const currentIndex = modes.indexOf(this.repeatMode);
|
||||
this.repeatMode = modes[(currentIndex + 1) % modes.length];
|
||||
|
||||
const repeatBtn = document.getElementById('repeat-btn');
|
||||
repeatBtn.dataset.mode = this.repeatMode;
|
||||
|
||||
// Update visual indicator
|
||||
if (this.repeatMode === 'off') {
|
||||
repeatBtn.style.color = '';
|
||||
} else if (this.repeatMode === 'all') {
|
||||
repeatBtn.style.color = 'var(--neon-cyan)';
|
||||
} else {
|
||||
repeatBtn.style.color = 'var(--neon-pink)';
|
||||
}
|
||||
}
|
||||
|
||||
setVolume(value) {
|
||||
this.volume = value;
|
||||
this.audio.volume = value;
|
||||
document.getElementById('volume-level').textContent = Math.round(value * 100) + '%';
|
||||
|
||||
// Update volume icon
|
||||
const volumeBtn = document.getElementById('volume-btn');
|
||||
if (value === 0) {
|
||||
volumeBtn.innerHTML = '<svg viewBox="0 0 24 24" class="control-icon"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>';
|
||||
} else if (value < 0.5) {
|
||||
volumeBtn.innerHTML = '<svg viewBox="0 0 24 24" class="control-icon"><path d="M7 9v6h4l5 5V4l-5 5H7z"/></svg>';
|
||||
} else {
|
||||
volumeBtn.innerHTML = '<svg viewBox="0 0 24 24" class="control-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>';
|
||||
}
|
||||
}
|
||||
|
||||
seek(value) {
|
||||
const duration = this.parseDuration(this.playlist[this.currentTrackIndex].duration);
|
||||
const seekTime = (value / 100) * duration;
|
||||
|
||||
// In real implementation, this would seek the audio
|
||||
// this.audio.currentTime = seekTime;
|
||||
|
||||
document.getElementById('progress-fill').style.width = value + '%';
|
||||
document.getElementById('current-time').textContent = this.formatTime(seekTime);
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
const progress = (this.audio.currentTime / this.audio.duration) * 100;
|
||||
document.getElementById('progress-fill').style.width = progress + '%';
|
||||
document.getElementById('seek-slider').value = progress;
|
||||
document.getElementById('current-time').textContent = this.formatTime(this.audio.currentTime);
|
||||
}
|
||||
|
||||
updateDuration() {
|
||||
document.getElementById('duration-time').textContent = this.formatTime(this.audio.duration);
|
||||
}
|
||||
|
||||
setEQPreset(preset) {
|
||||
// Update active state
|
||||
document.querySelectorAll('.eq-preset').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-preset="${preset}"]`).classList.add('active');
|
||||
|
||||
// In real implementation, this would apply audio filters
|
||||
console.log(`EQ preset set to: ${preset}`);
|
||||
}
|
||||
|
||||
renderPlaylist() {
|
||||
const playlistElement = document.getElementById('playlist');
|
||||
playlistElement.innerHTML = '';
|
||||
|
||||
this.playlist.forEach((track, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'playlist-item';
|
||||
li.innerHTML = `
|
||||
<span class="track-number">${(index + 1).toString().padStart(2, '0')}</span>
|
||||
<div class="track-details">
|
||||
<span class="track-name">${track.title}</span>
|
||||
<span class="track-duration">${track.duration}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
li.addEventListener('click', () => {
|
||||
this.loadTrack(index);
|
||||
this.play();
|
||||
});
|
||||
|
||||
playlistElement.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
updatePlaylistHighlight() {
|
||||
document.querySelectorAll('.playlist-item').forEach((item, index) => {
|
||||
if (index === this.currentTrackIndex) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addTrack() {
|
||||
// In real implementation, this would open a file picker or dialog
|
||||
alert('Add track functionality would open a file picker in a real application');
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
if (!this.isPlaying || !this.analyser) return;
|
||||
|
||||
// Get frequency data
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
|
||||
// Clear canvas
|
||||
this.ctx.fillStyle = 'rgba(22, 22, 22, 0.2)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw waveform
|
||||
const barWidth = (this.canvas.width / this.dataArray.length) * 2.5;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < this.dataArray.length; i++) {
|
||||
// Generate synthetic data for demo
|
||||
const barHeight = this.isPlaying ?
|
||||
Math.sin(Date.now() * 0.001 + i * 0.5) * 50 + Math.random() * 100 :
|
||||
0;
|
||||
|
||||
// Create gradient
|
||||
const gradient = this.ctx.createLinearGradient(0, this.canvas.height, 0, this.canvas.height - barHeight);
|
||||
gradient.addColorStop(0, '#ff006e');
|
||||
gradient.addColorStop(0.5, '#00f5ff');
|
||||
gradient.addColorStop(1, '#8338ec');
|
||||
|
||||
this.ctx.fillStyle = gradient;
|
||||
this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
|
||||
|
||||
// Add glow effect
|
||||
this.ctx.shadowBlur = 20;
|
||||
this.ctx.shadowColor = '#00f5ff';
|
||||
|
||||
x += barWidth + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize player when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const player = new NeonWavePlayer();
|
||||
|
||||
// Add glitch effect on hover
|
||||
const playerContainer = document.querySelector('.player-container');
|
||||
playerContainer.addEventListener('mouseenter', () => {
|
||||
const glitchOverlay = document.querySelector('.glitch-overlay');
|
||||
glitchOverlay.style.animation = 'glitch 2s infinite';
|
||||
});
|
||||
|
||||
playerContainer.addEventListener('mouseleave', () => {
|
||||
const glitchOverlay = document.querySelector('.glitch-overlay');
|
||||
glitchOverlay.style.animation = 'glitch 10s infinite';
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,646 @@
|
|||
/* Cyberpunk Neon Theme - CSS Custom Properties */
|
||||
:root {
|
||||
/* Neon Color Palette */
|
||||
--neon-pink: #ff006e;
|
||||
--neon-cyan: #00f5ff;
|
||||
--neon-purple: #8338ec;
|
||||
--neon-yellow: #ffbe0b;
|
||||
--neon-green: #3bff00;
|
||||
|
||||
/* Dark Base Colors */
|
||||
--bg-primary: #0a0a0a;
|
||||
--bg-secondary: #161616;
|
||||
--bg-tertiary: #1e1e1e;
|
||||
--bg-glass: rgba(10, 10, 10, 0.85);
|
||||
|
||||
/* Glow Effects */
|
||||
--glow-pink: 0 0 20px rgba(255, 0, 110, 0.5), 0 0 40px rgba(255, 0, 110, 0.3);
|
||||
--glow-cyan: 0 0 20px rgba(0, 245, 255, 0.5), 0 0 40px rgba(0, 245, 255, 0.3);
|
||||
--glow-purple: 0 0 20px rgba(131, 56, 236, 0.5), 0 0 40px rgba(131, 56, 236, 0.3);
|
||||
|
||||
/* Typography */
|
||||
--font-primary: 'Orbitron', monospace;
|
||||
--font-secondary: 'Roboto Mono', monospace;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.5rem;
|
||||
--spacing-sm: 1rem;
|
||||
--spacing-md: 1.5rem;
|
||||
--spacing-lg: 2rem;
|
||||
--spacing-xl: 3rem;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.2s ease;
|
||||
--transition-medium: 0.4s ease;
|
||||
--transition-slow: 0.6s ease;
|
||||
}
|
||||
|
||||
/* Font Imports */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Roboto+Mono:wght@300;400;700&display=swap');
|
||||
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-secondary);
|
||||
background: var(--bg-primary);
|
||||
color: var(--neon-cyan);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Animated Background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(255, 0, 110, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(0, 245, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(131, 56, 236, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Main Player Container */
|
||||
.media-player {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player-container {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 0, 110, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow:
|
||||
0 0 50px rgba(255, 0, 110, 0.2),
|
||||
inset 0 0 50px rgba(0, 245, 255, 0.1);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Glitch Effect Overlay */
|
||||
.glitch-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: glitch 10s infinite;
|
||||
}
|
||||
|
||||
@keyframes glitch {
|
||||
0%, 90%, 100% { opacity: 0; }
|
||||
92% {
|
||||
opacity: 1;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 0, 110, 0.1),
|
||||
rgba(255, 0, 110, 0.1) 2px,
|
||||
transparent 2px,
|
||||
transparent 4px
|
||||
);
|
||||
}
|
||||
94% {
|
||||
opacity: 1;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
96% {
|
||||
opacity: 1;
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Visualizer Section */
|
||||
.visualizer-section {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid rgba(0, 245, 255, 0.3);
|
||||
}
|
||||
|
||||
.waveform-canvas {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
display: block;
|
||||
filter: drop-shadow(0 0 10px var(--neon-cyan));
|
||||
}
|
||||
|
||||
/* Media Display */
|
||||
.media-display {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.album-art-container {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--neon-pink);
|
||||
box-shadow: var(--glow-pink);
|
||||
}
|
||||
|
||||
.album-art {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hologram-effect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent 30%,
|
||||
rgba(0, 245, 255, 0.1) 50%,
|
||||
transparent 70%
|
||||
);
|
||||
animation: hologram 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes hologram {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.track-info {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.track-title {
|
||||
font-family: var(--font-primary);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
color: var(--neon-pink);
|
||||
text-shadow: var(--glow-pink);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.track-artist {
|
||||
font-size: 1.25rem;
|
||||
color: var(--neon-cyan);
|
||||
margin-bottom: var(--spacing-md);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.track-metadata {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.metadata-tag {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--neon-purple);
|
||||
color: var(--neon-purple);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
box-shadow: var(--glow-purple);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.metadata-tag:hover {
|
||||
background: var(--neon-purple);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Progress Section */
|
||||
.progress-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.time-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.time-current,
|
||||
.time-duration {
|
||||
font-family: var(--font-primary);
|
||||
font-size: 1rem;
|
||||
color: var(--neon-green);
|
||||
text-shadow: 0 0 10px rgba(59, 255, 0, 0.5);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid rgba(0, 245, 255, 0.3);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--neon-pink), var(--neon-cyan));
|
||||
width: 0%;
|
||||
transition: width 0.1s linear;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-glow {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--neon-cyan);
|
||||
border-radius: 50%;
|
||||
filter: blur(10px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.seek-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Control Panel */
|
||||
.control-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.main-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px solid var(--neon-cyan);
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: var(--glow-cyan);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.control-btn-primary {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-color: var(--neon-pink);
|
||||
background: linear-gradient(135deg, var(--bg-tertiary), var(--bg-secondary));
|
||||
}
|
||||
|
||||
.control-btn-primary:hover {
|
||||
box-shadow: var(--glow-pink);
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.control-btn-primary .control-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.control-btn[data-active="true"] {
|
||||
border-color: var(--neon-green);
|
||||
color: var(--neon-green);
|
||||
box-shadow: var(--glow-cyan);
|
||||
}
|
||||
|
||||
/* Audio Controls */
|
||||
.audio-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(131, 56, 236, 0.3);
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
border: 1px solid rgba(255, 0, 110, 0.3);
|
||||
}
|
||||
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--neon-pink);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--glow-pink);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.volume-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.volume-level {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
right: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--neon-yellow);
|
||||
font-family: var(--font-primary);
|
||||
}
|
||||
|
||||
.equalizer {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.eq-preset {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--neon-purple);
|
||||
color: var(--neon-purple);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: 5px;
|
||||
font-family: var(--font-primary);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.eq-preset:hover,
|
||||
.eq-preset.active {
|
||||
background: var(--neon-purple);
|
||||
color: var(--bg-primary);
|
||||
box-shadow: var(--glow-purple);
|
||||
}
|
||||
|
||||
/* Playlist Section */
|
||||
.playlist-section {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid rgba(0, 245, 255, 0.3);
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.playlist-title {
|
||||
font-family: var(--font-primary);
|
||||
font-size: 1.5rem;
|
||||
color: var(--neon-cyan);
|
||||
text-shadow: var(--glow-cyan);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.playlist-action {
|
||||
background: transparent;
|
||||
border: 1px solid var(--neon-green);
|
||||
color: var(--neon-green);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.playlist-action:hover {
|
||||
background: var(--neon-green);
|
||||
color: var(--bg-primary);
|
||||
box-shadow: 0 0 20px rgba(59, 255, 0, 0.5);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.playlist-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--neon-purple) var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.playlist-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.playlist-container::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.playlist-container::-webkit-scrollbar-thumb {
|
||||
background: var(--neon-purple);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.playlist {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.playlist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm);
|
||||
border-bottom: 1px solid rgba(255, 0, 110, 0.1);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.playlist-item:hover {
|
||||
background: rgba(0, 245, 255, 0.1);
|
||||
padding-left: var(--spacing-md);
|
||||
}
|
||||
|
||||
.playlist-item.active {
|
||||
background: rgba(255, 0, 110, 0.1);
|
||||
border-left: 3px solid var(--neon-pink);
|
||||
}
|
||||
|
||||
.playlist-item.active::before {
|
||||
content: '▶';
|
||||
position: absolute;
|
||||
left: var(--spacing-sm);
|
||||
color: var(--neon-pink);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.track-number {
|
||||
min-width: 30px;
|
||||
color: var(--neon-purple);
|
||||
font-family: var(--font-primary);
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.track-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.track-name {
|
||||
color: var(--neon-cyan);
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.track-duration {
|
||||
color: var(--neon-green);
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.media-player {
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.player-container {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.media-display {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.album-art-container {
|
||||
margin: 0 auto;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.track-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.main-controls {
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.control-btn-primary {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.audio-controls {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes neon-flicker {
|
||||
0%, 100% { opacity: 1; }
|
||||
41.99% { opacity: 1; }
|
||||
42% { opacity: 0.8; }
|
||||
43% { opacity: 1; }
|
||||
45% { opacity: 0.3; }
|
||||
46% { opacity: 1; }
|
||||
}
|
||||
|
||||
.track-title {
|
||||
animation: neon-flicker 5s infinite;
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Art Deco Time Manager - UI Hybrid 5</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="time-manager-container">
|
||||
<header class="tm-header">
|
||||
<div class="art-deco-frame">
|
||||
<h1 class="tm-title">Chronos Suite</h1>
|
||||
<p class="tm-subtitle">Your Elegant Time Companion</p>
|
||||
</div>
|
||||
<div class="tm-current-time">
|
||||
<div class="time-display" id="currentTime">00:00:00</div>
|
||||
<div class="date-display" id="currentDate">Monday, January 1, 1920</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tm-navigation">
|
||||
<button class="nav-tab active" data-view="calendar">Calendar</button>
|
||||
<button class="nav-tab" data-view="timer">Timer</button>
|
||||
<button class="nav-tab" data-view="schedule">Schedule</button>
|
||||
<button class="nav-tab" data-view="timezone">World Time</button>
|
||||
<button class="nav-tab" data-view="deadlines">Deadlines</button>
|
||||
</nav>
|
||||
|
||||
<main class="tm-content">
|
||||
<section class="view-panel active" id="calendarView">
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<button class="calendar-nav" id="prevMonth">◄</button>
|
||||
<h2 class="calendar-month-year" id="monthYear">January 1920</h2>
|
||||
<button class="calendar-nav" id="nextMonth">►</button>
|
||||
</div>
|
||||
<div class="calendar-grid" id="calendarGrid">
|
||||
<div class="day-header">Sun</div>
|
||||
<div class="day-header">Mon</div>
|
||||
<div class="day-header">Tue</div>
|
||||
<div class="day-header">Wed</div>
|
||||
<div class="day-header">Thu</div>
|
||||
<div class="day-header">Fri</div>
|
||||
<div class="day-header">Sat</div>
|
||||
</div>
|
||||
<div class="events-panel">
|
||||
<h3 class="events-title">Today's Affairs</h3>
|
||||
<ul class="events-list" id="eventsList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view-panel" id="timerView">
|
||||
<div class="timer-container">
|
||||
<div class="timer-display">
|
||||
<div class="timer-digits" id="timerDisplay">00:00:00</div>
|
||||
</div>
|
||||
<div class="timer-controls">
|
||||
<button class="timer-btn" id="startTimer">Start</button>
|
||||
<button class="timer-btn" id="pauseTimer">Pause</button>
|
||||
<button class="timer-btn" id="resetTimer">Reset</button>
|
||||
</div>
|
||||
<div class="timer-presets">
|
||||
<h3>Quick Sets</h3>
|
||||
<div class="preset-buttons">
|
||||
<button class="preset-btn" data-minutes="5">5 min</button>
|
||||
<button class="preset-btn" data-minutes="15">15 min</button>
|
||||
<button class="preset-btn" data-minutes="30">30 min</button>
|
||||
<button class="preset-btn" data-minutes="60">1 hour</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view-panel" id="scheduleView">
|
||||
<div class="schedule-container">
|
||||
<h2 class="schedule-title">Daily Programme</h2>
|
||||
<form class="schedule-form" id="scheduleForm">
|
||||
<input type="time" class="schedule-input" id="scheduleTime" required>
|
||||
<input type="text" class="schedule-input" id="scheduleTask" placeholder="Enter appointment details" required>
|
||||
<button type="submit" class="schedule-btn">Add to Schedule</button>
|
||||
</form>
|
||||
<div class="schedule-timeline" id="scheduleTimeline"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view-panel" id="timezoneView">
|
||||
<div class="timezone-container">
|
||||
<h2 class="timezone-title">World Clock</h2>
|
||||
<div class="timezone-grid" id="timezoneGrid">
|
||||
<div class="timezone-card" data-tz="America/New_York">
|
||||
<h3>New York</h3>
|
||||
<div class="tz-time"></div>
|
||||
</div>
|
||||
<div class="timezone-card" data-tz="Europe/London">
|
||||
<h3>London</h3>
|
||||
<div class="tz-time"></div>
|
||||
</div>
|
||||
<div class="timezone-card" data-tz="Europe/Paris">
|
||||
<h3>Paris</h3>
|
||||
<div class="tz-time"></div>
|
||||
</div>
|
||||
<div class="timezone-card" data-tz="Asia/Tokyo">
|
||||
<h3>Tokyo</h3>
|
||||
<div class="tz-time"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timezone-converter">
|
||||
<h3>Time Converter</h3>
|
||||
<input type="time" class="converter-input" id="converterTime">
|
||||
<select class="converter-select" id="fromTimezone">
|
||||
<option value="local">Local Time</option>
|
||||
<option value="America/New_York">New York</option>
|
||||
<option value="Europe/London">London</option>
|
||||
<option value="Asia/Tokyo">Tokyo</option>
|
||||
</select>
|
||||
<button class="converter-btn" id="convertTime">Convert</button>
|
||||
<div class="converter-results" id="converterResults"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view-panel" id="deadlinesView">
|
||||
<div class="deadlines-container">
|
||||
<h2 class="deadlines-title">Important Deadlines</h2>
|
||||
<form class="deadline-form" id="deadlineForm">
|
||||
<input type="text" class="deadline-input" id="deadlineTitle" placeholder="Deadline title" required>
|
||||
<input type="datetime-local" class="deadline-input" id="deadlineDate" required>
|
||||
<select class="deadline-input" id="deadlinePriority">
|
||||
<option value="low">Low Priority</option>
|
||||
<option value="medium">Medium Priority</option>
|
||||
<option value="high">High Priority</option>
|
||||
</select>
|
||||
<button type="submit" class="deadline-btn">Add Deadline</button>
|
||||
</form>
|
||||
<div class="deadlines-list" id="deadlinesList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="tm-footer">
|
||||
<div class="art-deco-ornament"></div>
|
||||
<p class="footer-text">Tempus Fugit · Carpe Diem</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,541 @@
|
|||
// Art Deco Time Manager - Interactive JavaScript
|
||||
|
||||
class TimeManager {
|
||||
constructor() {
|
||||
this.currentView = 'calendar';
|
||||
this.timerInterval = null;
|
||||
this.timerSeconds = 0;
|
||||
this.timerRunning = false;
|
||||
this.selectedDate = new Date();
|
||||
this.events = this.loadFromStorage('events') || {};
|
||||
this.schedule = this.loadFromStorage('schedule') || [];
|
||||
this.deadlines = this.loadFromStorage('deadlines') || [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupNavigationListeners();
|
||||
this.setupTimeDisplay();
|
||||
this.setupCalendar();
|
||||
this.setupTimer();
|
||||
this.setupSchedule();
|
||||
this.setupTimezone();
|
||||
this.setupDeadlines();
|
||||
this.updateWorldClocks();
|
||||
setInterval(() => this.updateWorldClocks(), 1000);
|
||||
}
|
||||
|
||||
// Navigation Management
|
||||
setupNavigationListeners() {
|
||||
const navTabs = document.querySelectorAll('.nav-tab');
|
||||
navTabs.forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
this.switchView(e.target.dataset.view);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
switchView(viewName) {
|
||||
// Update navigation tabs
|
||||
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.view === viewName);
|
||||
});
|
||||
|
||||
// Update view panels
|
||||
document.querySelectorAll('.view-panel').forEach(panel => {
|
||||
panel.classList.toggle('active', panel.id === `${viewName}View`);
|
||||
});
|
||||
|
||||
this.currentView = viewName;
|
||||
}
|
||||
|
||||
// Time Display
|
||||
setupTimeDisplay() {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
const dateStr = now.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
document.getElementById('currentTime').textContent = timeStr;
|
||||
document.getElementById('currentDate').textContent = dateStr;
|
||||
};
|
||||
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
}
|
||||
|
||||
// Calendar Functionality
|
||||
setupCalendar() {
|
||||
this.renderCalendar();
|
||||
|
||||
document.getElementById('prevMonth').addEventListener('click', () => {
|
||||
this.selectedDate.setMonth(this.selectedDate.getMonth() - 1);
|
||||
this.renderCalendar();
|
||||
});
|
||||
|
||||
document.getElementById('nextMonth').addEventListener('click', () => {
|
||||
this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
|
||||
this.renderCalendar();
|
||||
});
|
||||
}
|
||||
|
||||
renderCalendar() {
|
||||
const year = this.selectedDate.getFullYear();
|
||||
const month = this.selectedDate.getMonth();
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const daysInPrevMonth = new Date(year, month, 0).getDate();
|
||||
|
||||
// Update month/year display
|
||||
document.getElementById('monthYear').textContent =
|
||||
new Date(year, month).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
// Clear existing calendar days
|
||||
const grid = document.getElementById('calendarGrid');
|
||||
const existingDays = grid.querySelectorAll('.calendar-day');
|
||||
existingDays.forEach(day => day.remove());
|
||||
|
||||
// Add previous month's trailing days
|
||||
for (let i = firstDay - 1; i >= 0; i--) {
|
||||
const dayEl = this.createCalendarDay(
|
||||
daysInPrevMonth - i,
|
||||
month - 1,
|
||||
year,
|
||||
true
|
||||
);
|
||||
grid.appendChild(dayEl);
|
||||
}
|
||||
|
||||
// Add current month's days
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dayEl = this.createCalendarDay(day, month, year, false);
|
||||
grid.appendChild(dayEl);
|
||||
}
|
||||
|
||||
// Add next month's leading days
|
||||
const totalCells = grid.children.length - 7; // Subtract header row
|
||||
const remainingCells = 42 - totalCells; // 6 weeks * 7 days
|
||||
for (let day = 1; day <= remainingCells; day++) {
|
||||
const dayEl = this.createCalendarDay(day, month + 1, year, true);
|
||||
grid.appendChild(dayEl);
|
||||
}
|
||||
|
||||
this.renderEvents();
|
||||
}
|
||||
|
||||
createCalendarDay(day, month, year, isOtherMonth) {
|
||||
const dayEl = document.createElement('div');
|
||||
dayEl.className = 'calendar-day';
|
||||
if (isOtherMonth) dayEl.classList.add('other-month');
|
||||
|
||||
const date = new Date(year, month, day);
|
||||
const today = new Date();
|
||||
if (this.isSameDay(date, today)) {
|
||||
dayEl.classList.add('today');
|
||||
}
|
||||
|
||||
dayEl.textContent = day;
|
||||
dayEl.addEventListener('click', () => this.selectDate(date));
|
||||
|
||||
// Check for events
|
||||
const dateKey = this.getDateKey(date);
|
||||
if (this.events[dateKey]) {
|
||||
dayEl.style.borderBottom = '3px solid var(--deco-gold)';
|
||||
}
|
||||
|
||||
return dayEl;
|
||||
}
|
||||
|
||||
selectDate(date) {
|
||||
this.selectedDate = date;
|
||||
this.renderEvents();
|
||||
}
|
||||
|
||||
renderEvents() {
|
||||
const eventsList = document.getElementById('eventsList');
|
||||
eventsList.innerHTML = '';
|
||||
|
||||
const dateKey = this.getDateKey(this.selectedDate);
|
||||
const dayEvents = this.events[dateKey] || [];
|
||||
|
||||
if (dayEvents.length === 0) {
|
||||
eventsList.innerHTML = '<li class="event-item">No appointments scheduled</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
dayEvents.forEach(event => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'event-item';
|
||||
li.textContent = event;
|
||||
eventsList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Timer Functionality
|
||||
setupTimer() {
|
||||
document.getElementById('startTimer').addEventListener('click', () => this.startTimer());
|
||||
document.getElementById('pauseTimer').addEventListener('click', () => this.pauseTimer());
|
||||
document.getElementById('resetTimer').addEventListener('click', () => this.resetTimer());
|
||||
|
||||
// Preset buttons
|
||||
document.querySelectorAll('.preset-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const minutes = parseInt(e.target.dataset.minutes);
|
||||
this.timerSeconds = minutes * 60;
|
||||
this.updateTimerDisplay();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
if (!this.timerRunning) {
|
||||
this.timerRunning = true;
|
||||
this.timerInterval = setInterval(() => {
|
||||
if (this.timerSeconds > 0) {
|
||||
this.timerSeconds--;
|
||||
this.updateTimerDisplay();
|
||||
} else {
|
||||
this.pauseTimer();
|
||||
this.showNotification('Timer Complete!');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
pauseTimer() {
|
||||
this.timerRunning = false;
|
||||
if (this.timerInterval) {
|
||||
clearInterval(this.timerInterval);
|
||||
this.timerInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
this.pauseTimer();
|
||||
this.timerSeconds = 0;
|
||||
this.updateTimerDisplay();
|
||||
}
|
||||
|
||||
updateTimerDisplay() {
|
||||
const hours = Math.floor(this.timerSeconds / 3600);
|
||||
const minutes = Math.floor((this.timerSeconds % 3600) / 60);
|
||||
const seconds = this.timerSeconds % 60;
|
||||
|
||||
const display = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
document.getElementById('timerDisplay').textContent = display;
|
||||
}
|
||||
|
||||
// Schedule Functionality
|
||||
setupSchedule() {
|
||||
const form = document.getElementById('scheduleForm');
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.addScheduleItem();
|
||||
});
|
||||
|
||||
this.renderSchedule();
|
||||
}
|
||||
|
||||
addScheduleItem() {
|
||||
const time = document.getElementById('scheduleTime').value;
|
||||
const task = document.getElementById('scheduleTask').value;
|
||||
|
||||
if (time && task) {
|
||||
this.schedule.push({ time, task });
|
||||
this.schedule.sort((a, b) => a.time.localeCompare(b.time));
|
||||
this.saveToStorage('schedule', this.schedule);
|
||||
this.renderSchedule();
|
||||
|
||||
// Reset form
|
||||
document.getElementById('scheduleForm').reset();
|
||||
}
|
||||
}
|
||||
|
||||
renderSchedule() {
|
||||
const timeline = document.getElementById('scheduleTimeline');
|
||||
timeline.innerHTML = '';
|
||||
|
||||
if (this.schedule.length === 0) {
|
||||
timeline.innerHTML = '<div class="timeline-item">No appointments scheduled</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.schedule.forEach((item, index) => {
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = 'timeline-item';
|
||||
itemEl.innerHTML = `
|
||||
<span class="timeline-time">${item.time}</span>
|
||||
<span class="timeline-task">${item.task}</span>
|
||||
`;
|
||||
timeline.appendChild(itemEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Timezone Functionality
|
||||
setupTimezone() {
|
||||
document.getElementById('convertTime').addEventListener('click', () => {
|
||||
this.convertTime();
|
||||
});
|
||||
}
|
||||
|
||||
updateWorldClocks() {
|
||||
const cards = document.querySelectorAll('.timezone-card');
|
||||
cards.forEach(card => {
|
||||
const timezone = card.dataset.tz;
|
||||
const timeEl = card.querySelector('.tz-time');
|
||||
const time = new Date().toLocaleTimeString('en-US', {
|
||||
timeZone: timezone,
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
timeEl.textContent = time;
|
||||
});
|
||||
}
|
||||
|
||||
convertTime() {
|
||||
const time = document.getElementById('converterTime').value;
|
||||
const fromTz = document.getElementById('fromTimezone').value;
|
||||
const results = document.getElementById('converterResults');
|
||||
|
||||
if (!time) {
|
||||
results.innerHTML = '<p>Please select a time to convert</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
const date = new Date();
|
||||
date.setHours(hours, minutes, 0, 0);
|
||||
|
||||
const zones = [
|
||||
{ name: 'New York', tz: 'America/New_York' },
|
||||
{ name: 'London', tz: 'Europe/London' },
|
||||
{ name: 'Paris', tz: 'Europe/Paris' },
|
||||
{ name: 'Tokyo', tz: 'Asia/Tokyo' }
|
||||
];
|
||||
|
||||
results.innerHTML = '<h4>Converted Times:</h4>';
|
||||
zones.forEach(zone => {
|
||||
const convertedTime = date.toLocaleTimeString('en-US', {
|
||||
timeZone: zone.tz,
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
results.innerHTML += `<p>${zone.name}: ${convertedTime}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Deadlines Functionality
|
||||
setupDeadlines() {
|
||||
const form = document.getElementById('deadlineForm');
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.addDeadline();
|
||||
});
|
||||
|
||||
this.renderDeadlines();
|
||||
setInterval(() => this.updateDeadlineCountdowns(), 60000); // Update every minute
|
||||
}
|
||||
|
||||
addDeadline() {
|
||||
const title = document.getElementById('deadlineTitle').value;
|
||||
const date = document.getElementById('deadlineDate').value;
|
||||
const priority = document.getElementById('deadlinePriority').value;
|
||||
|
||||
if (title && date) {
|
||||
const deadline = {
|
||||
id: Date.now(),
|
||||
title,
|
||||
date: new Date(date),
|
||||
priority
|
||||
};
|
||||
|
||||
this.deadlines.push(deadline);
|
||||
this.deadlines.sort((a, b) => a.date - b.date);
|
||||
this.saveToStorage('deadlines', this.deadlines);
|
||||
this.renderDeadlines();
|
||||
|
||||
// Reset form
|
||||
document.getElementById('deadlineForm').reset();
|
||||
}
|
||||
}
|
||||
|
||||
renderDeadlines() {
|
||||
const list = document.getElementById('deadlinesList');
|
||||
list.innerHTML = '';
|
||||
|
||||
if (this.deadlines.length === 0) {
|
||||
list.innerHTML = '<div class="deadline-item">No deadlines set</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.deadlines.forEach(deadline => {
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = `deadline-item ${deadline.priority}-priority`;
|
||||
|
||||
const countdown = this.getCountdown(deadline.date);
|
||||
|
||||
itemEl.innerHTML = `
|
||||
<div class="deadline-header">
|
||||
<span class="deadline-title-text">${deadline.title}</span>
|
||||
<span class="deadline-priority">${deadline.priority.toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="deadline-date">${deadline.date.toLocaleString()}</div>
|
||||
<div class="deadline-countdown">${countdown}</div>
|
||||
<button class="deadline-remove" data-id="${deadline.id}">×</button>
|
||||
`;
|
||||
|
||||
const removeBtn = itemEl.querySelector('.deadline-remove');
|
||||
removeBtn.addEventListener('click', () => this.removeDeadline(deadline.id));
|
||||
|
||||
list.appendChild(itemEl);
|
||||
});
|
||||
}
|
||||
|
||||
removeDeadline(id) {
|
||||
this.deadlines = this.deadlines.filter(d => d.id !== id);
|
||||
this.saveToStorage('deadlines', this.deadlines);
|
||||
this.renderDeadlines();
|
||||
}
|
||||
|
||||
updateDeadlineCountdowns() {
|
||||
if (this.currentView === 'deadlines') {
|
||||
this.renderDeadlines();
|
||||
}
|
||||
}
|
||||
|
||||
getCountdown(targetDate) {
|
||||
const now = new Date();
|
||||
const diff = targetDate - now;
|
||||
|
||||
if (diff < 0) {
|
||||
return 'Overdue';
|
||||
}
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (days > 0) {
|
||||
return `${days} days, ${hours} hours remaining`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours} hours, ${minutes} minutes remaining`;
|
||||
} else {
|
||||
return `${minutes} minutes remaining`;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
isSameDay(date1, date2) {
|
||||
return date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate();
|
||||
}
|
||||
|
||||
getDateKey(date) {
|
||||
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
}
|
||||
|
||||
showNotification(message) {
|
||||
// Create a temporary notification
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 20px;
|
||||
background: var(--deco-gold);
|
||||
color: var(--deco-black);
|
||||
border: 2px solid var(--deco-black);
|
||||
font-family: var(--font-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.5s ease;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.5s ease';
|
||||
setTimeout(() => notification.remove(), 500);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Storage Functions
|
||||
saveToStorage(key, data) {
|
||||
try {
|
||||
localStorage.setItem(`timeManager_${key}`, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error('Failed to save to storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
loadFromStorage(key) {
|
||||
try {
|
||||
const data = localStorage.getItem(`timeManager_${key}`);
|
||||
if (data) {
|
||||
const parsed = JSON.parse(data);
|
||||
// Convert date strings back to Date objects for deadlines
|
||||
if (key === 'deadlines') {
|
||||
return parsed.map(d => ({
|
||||
...d,
|
||||
date: new Date(d.date)
|
||||
}));
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load from storage:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Add animations to stylesheet dynamically
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Initialize Time Manager when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new TimeManager();
|
||||
});
|
||||
|
|
@ -0,0 +1,710 @@
|
|||
/* Art Deco Elegance Theme - Time Manager Styles */
|
||||
|
||||
:root {
|
||||
/* Art Deco Color Palette */
|
||||
--deco-gold: #D4AF37;
|
||||
--deco-light-gold: #F4E4BC;
|
||||
--deco-dark-gold: #B8941F;
|
||||
--deco-black: #1A1A1A;
|
||||
--deco-cream: #F5F2E8;
|
||||
--deco-gray: #4A4A4A;
|
||||
--deco-accent: #8B4513;
|
||||
|
||||
/* Typography */
|
||||
--font-display: 'Playfair Display', 'Georgia', serif;
|
||||
--font-body: 'Crimson Text', 'Times New Roman', serif;
|
||||
--font-accent: 'Oswald', 'Arial', sans-serif;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 2rem;
|
||||
--spacing-xl: 3rem;
|
||||
|
||||
/* Borders and Ornaments */
|
||||
--border-ornate: 2px solid var(--deco-gold);
|
||||
--shadow-elegant: 0 4px 20px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background-color: var(--deco-black);
|
||||
color: var(--deco-cream);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
rgba(212, 175, 55, 0.05) 10px,
|
||||
rgba(212, 175, 55, 0.05) 20px
|
||||
);
|
||||
}
|
||||
|
||||
.time-manager-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.tm-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.art-deco-frame {
|
||||
display: inline-block;
|
||||
padding: var(--spacing-lg);
|
||||
position: relative;
|
||||
background: var(--deco-black);
|
||||
border: 3px solid var(--deco-gold);
|
||||
clip-path: polygon(
|
||||
0 10px, 10px 0,
|
||||
calc(100% - 10px) 0, 100% 10px,
|
||||
100% calc(100% - 10px), calc(100% - 10px) 100%,
|
||||
10px 100%, 0 calc(100% - 10px)
|
||||
);
|
||||
}
|
||||
|
||||
.art-deco-frame::before,
|
||||
.art-deco-frame::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--deco-gold);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.art-deco-frame::before {
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
right: -6px;
|
||||
bottom: -6px;
|
||||
}
|
||||
|
||||
.tm-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 3rem;
|
||||
color: var(--deco-gold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.tm-subtitle {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 1.2rem;
|
||||
color: var(--deco-light-gold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2rem;
|
||||
}
|
||||
|
||||
.tm-current-time {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: linear-gradient(135deg, var(--deco-black) 0%, var(--deco-gray) 100%);
|
||||
border: var(--border-ornate);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 2.5rem;
|
||||
color: var(--deco-gold);
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.date-display {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.2rem;
|
||||
color: var(--deco-cream);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Navigation Styles */
|
||||
.tm-navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--deco-black);
|
||||
border-top: var(--border-ornate);
|
||||
border-bottom: var(--border-ornate);
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
background: transparent;
|
||||
color: var(--deco-cream);
|
||||
border: 1px solid var(--deco-gold);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-tab::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--deco-gold);
|
||||
transition: left 0.3s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.nav-tab:hover::before,
|
||||
.nav-tab.active::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.nav-tab:hover,
|
||||
.nav-tab.active {
|
||||
color: var(--deco-black);
|
||||
}
|
||||
|
||||
/* Content Panel Styles */
|
||||
.tm-content {
|
||||
background: var(--deco-black);
|
||||
border: var(--border-ornate);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-elegant);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.view-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view-panel.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Calendar Styles */
|
||||
.calendar-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 1.5rem;
|
||||
background: transparent;
|
||||
color: var(--deco-gold);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-sm);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-nav:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.calendar-month-year {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.8rem;
|
||||
color: var(--deco-gold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2rem;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1px;
|
||||
background: var(--deco-gold);
|
||||
border: var(--border-ornate);
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
font-family: var(--font-accent);
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--deco-gray);
|
||||
color: var(--deco-gold);
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
aspect-ratio: 1;
|
||||
background: var(--deco-black);
|
||||
border: 1px solid transparent;
|
||||
padding: var(--spacing-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background: var(--deco-gray);
|
||||
border-color: var(--deco-gold);
|
||||
}
|
||||
|
||||
.calendar-day.selected {
|
||||
background: var(--deco-gold);
|
||||
color: var(--deco-black);
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
border: 2px solid var(--deco-gold);
|
||||
}
|
||||
|
||||
.calendar-day.other-month {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.events-panel {
|
||||
margin-top: var(--spacing-xl);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
}
|
||||
|
||||
.events-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.5rem;
|
||||
color: var(--deco-gold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.events-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
background: var(--deco-black);
|
||||
border-left: 4px solid var(--deco-gold);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
/* Timer Styles */
|
||||
.timer-container {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
padding: var(--spacing-xl);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timer-display::before,
|
||||
.timer-display::after {
|
||||
content: '❦';
|
||||
position: absolute;
|
||||
font-size: 2rem;
|
||||
color: var(--deco-gold);
|
||||
}
|
||||
|
||||
.timer-display::before {
|
||||
top: var(--spacing-md);
|
||||
left: var(--spacing-md);
|
||||
}
|
||||
|
||||
.timer-display::after {
|
||||
bottom: var(--spacing-md);
|
||||
right: var(--spacing-md);
|
||||
}
|
||||
|
||||
.timer-digits {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 4rem;
|
||||
color: var(--deco-gold);
|
||||
letter-spacing: 0.2rem;
|
||||
}
|
||||
|
||||
.timer-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.timer-btn,
|
||||
.preset-btn,
|
||||
.schedule-btn,
|
||||
.deadline-btn,
|
||||
.converter-btn {
|
||||
font-family: var(--font-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--deco-black);
|
||||
color: var(--deco-gold);
|
||||
border: var(--border-ornate);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timer-btn:hover,
|
||||
.preset-btn:hover,
|
||||
.schedule-btn:hover,
|
||||
.deadline-btn:hover,
|
||||
.converter-btn:hover {
|
||||
background: var(--deco-gold);
|
||||
color: var(--deco-black);
|
||||
}
|
||||
|
||||
.timer-presets {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-black);
|
||||
border: var(--border-ornate);
|
||||
}
|
||||
|
||||
.timer-presets h3 {
|
||||
font-family: var(--font-display);
|
||||
color: var(--deco-gold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Schedule Styles */
|
||||
.schedule-container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.schedule-title,
|
||||
.timezone-title,
|
||||
.deadlines-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 2rem;
|
||||
color: var(--deco-gold);
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2rem;
|
||||
}
|
||||
|
||||
.schedule-form,
|
||||
.deadline-form {
|
||||
display: grid;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
}
|
||||
|
||||
.schedule-input,
|
||||
.deadline-input,
|
||||
.converter-input,
|
||||
.converter-select {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--deco-black);
|
||||
color: var(--deco-cream);
|
||||
border: 1px solid var(--deco-gold);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.schedule-input:focus,
|
||||
.deadline-input:focus,
|
||||
.converter-input:focus,
|
||||
.converter-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--deco-light-gold);
|
||||
}
|
||||
|
||||
.schedule-timeline {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-black);
|
||||
border: var(--border-ornate);
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--deco-gray);
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-family: var(--font-accent);
|
||||
color: var(--deco-gold);
|
||||
margin-right: var(--spacing-lg);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.timeline-task {
|
||||
font-family: var(--font-body);
|
||||
color: var(--deco-cream);
|
||||
}
|
||||
|
||||
/* Timezone Styles */
|
||||
.timezone-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.timezone-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.timezone-card {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.timezone-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-elegant);
|
||||
}
|
||||
|
||||
.timezone-card h3 {
|
||||
font-family: var(--font-display);
|
||||
color: var(--deco-gold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
.tz-time {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 1.5rem;
|
||||
color: var(--deco-cream);
|
||||
}
|
||||
|
||||
.timezone-converter {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
}
|
||||
|
||||
.timezone-converter h3 {
|
||||
font-family: var(--font-display);
|
||||
color: var(--deco-gold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.converter-results {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--deco-black);
|
||||
border: 1px solid var(--deco-gold);
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Deadlines Styles */
|
||||
.deadlines-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.deadlines-list {
|
||||
display: grid;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.deadline-item {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--deco-gray);
|
||||
border: var(--border-ornate);
|
||||
position: relative;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.deadline-item:hover {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.deadline-item.high-priority {
|
||||
border-color: #FF6B6B;
|
||||
box-shadow: 0 0 20px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.deadline-item.medium-priority {
|
||||
border-color: #FFD93D;
|
||||
box-shadow: 0 0 20px rgba(255, 217, 61, 0.3);
|
||||
}
|
||||
|
||||
.deadline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.deadline-title-text {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.3rem;
|
||||
color: var(--deco-gold);
|
||||
}
|
||||
|
||||
.deadline-priority {
|
||||
font-family: var(--font-accent);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--deco-black);
|
||||
color: var(--deco-gold);
|
||||
border: 1px solid var(--deco-gold);
|
||||
}
|
||||
|
||||
.deadline-date {
|
||||
font-family: var(--font-body);
|
||||
color: var(--deco-cream);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.deadline-countdown {
|
||||
font-family: var(--font-accent);
|
||||
color: var(--deco-light-gold);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.deadline-remove {
|
||||
position: absolute;
|
||||
top: var(--spacing-sm);
|
||||
right: var(--spacing-sm);
|
||||
background: transparent;
|
||||
color: var(--deco-gold);
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.deadline-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
.tm-footer {
|
||||
text-align: center;
|
||||
margin-top: var(--spacing-xl);
|
||||
padding: var(--spacing-lg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.art-deco-ornament {
|
||||
width: 200px;
|
||||
height: 2px;
|
||||
background: var(--deco-gold);
|
||||
margin: 0 auto var(--spacing-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.art-deco-ornament::before,
|
||||
.art-deco-ornament::after {
|
||||
content: '◆';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--deco-gold);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.art-deco-ornament::before {
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.art-deco-ornament::after {
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-family: var(--font-body);
|
||||
font-style: italic;
|
||||
color: var(--deco-cream);
|
||||
letter-spacing: 0.1rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.tm-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.tm-navigation {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
font-size: 0.9rem;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
}
|
||||
|
||||
.timezone-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.timer-digits {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue