mirror of
				https://github.com/jaandrle/deka-dom-el
				synced 2025-10-30 05:29:15 +01:00 
			
		
		
		
	🔤 Examples
This commit is contained in:
		| @@ -116,7 +116,12 @@ format selector](https://jaandrle.github.io/deka-dom-el/) on the documentation s | ||||
| ### Documentation | ||||
|  | ||||
| - [**Interactive Guide**](https://jaandrle.github.io/deka-dom-el): WIP | ||||
| - [Examples](./examples/): TBD/WIP | ||||
| - [**Examples Gallery**](https://jaandrle.github.io/deka-dom-el/p15-examples.html): A comprehensive collection of code examples and case studies | ||||
|   - Interactive Form with Validation | ||||
|   - Data Dashboard with Charts | ||||
|   - Image Gallery with Lightbox | ||||
|   - Kanban Task Manager | ||||
|   - TodoMVC Implementation | ||||
|  | ||||
| ## Understanding Signals | ||||
|  | ||||
|   | ||||
							
								
								
									
										394
									
								
								docs/components/examples/case-studies/data-dashboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										394
									
								
								docs/components/examples/case-studies/data-dashboard.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,394 @@ | ||||
| /** | ||||
|  * Case Study: Data Dashboard with Charts | ||||
|  * | ||||
|  * This example demonstrates: | ||||
|  * - Integration with a third-party charting library | ||||
|  * - Data fetching and state management | ||||
|  * - Responsive layout design | ||||
|  * - Multiple interactive components working together | ||||
|  */ | ||||
|  | ||||
| import { el, on } from "deka-dom-el"; | ||||
| import { S } from "deka-dom-el/signals"; | ||||
|  | ||||
| /** | ||||
|  * Data Dashboard Component with Chart Integration | ||||
|  * @returns {HTMLElement} Dashboard element | ||||
|  */ | ||||
| export function DataDashboard() { | ||||
| 	// Mock data for demonstration | ||||
| 	const DATA = { | ||||
| 		sales: [42, 58, 65, 49, 72, 85, 63, 70, 78, 89, 95, 86], | ||||
| 		visitors: [1420, 1620, 1750, 1850, 2100, 2400, 2250, 2500, 2750, 2900, 3100, 3200], | ||||
| 		conversion: [2.9, 3.5, 3.7, 2.6, 3.4, 3.5, 2.8, 2.8, 2.8, 3.1, 3.0, 2.7], | ||||
| 		months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] | ||||
| 	}; | ||||
|  | ||||
| 	// Application state | ||||
| 	const selectedYear = S(2024); | ||||
| 	const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales')); | ||||
| 	const isLoading = S(false); | ||||
| 	const error = S(null); | ||||
|  | ||||
| 	// Filter options | ||||
| 	const years = [2022, 2023, 2024]; | ||||
| 	const dataTypes = [ | ||||
| 		{ id: 'sales', label: 'Sales', unit: 'K' }, | ||||
| 		{ id: 'visitors', label: 'Visitors', unit: '' }, | ||||
| 		{ id: 'conversion', label: 'Conversion Rate', unit: '%' } | ||||
| 	]; | ||||
|  | ||||
| 	// Computed values | ||||
| 	const selectedData = S(() => { | ||||
| 		return DATA[selectedDataType.get()]; | ||||
| 	}); | ||||
|  | ||||
| 	const currentDataType = S(() => { | ||||
| 		return dataTypes.find(type => type.id === selectedDataType.get()); | ||||
| 	}); | ||||
|  | ||||
| 	const totalValue = S(() => { | ||||
| 		const data = selectedData.get(); | ||||
| 		return data.reduce((sum, value) => sum + value, 0); | ||||
| 	}); | ||||
|  | ||||
| 	const averageValue = S(() => { | ||||
| 		const data = selectedData.get(); | ||||
| 		return data.reduce((sum, value) => sum + value, 0) / data.length; | ||||
| 	}); | ||||
|  | ||||
| 	const highestValue = S(() => { | ||||
| 		return Math.max(...selectedData.get()); | ||||
| 	}); | ||||
|  | ||||
| 	// Event handlers | ||||
| 	const onYearChange = on("change", e => { | ||||
| 		selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value)); | ||||
| 		loadData(); | ||||
| 	}); | ||||
|  | ||||
| 	const onDataTypeChange = on("click", e => { | ||||
| 		const type = /** @type {'sales' | 'visitors' | 'conversion'} */( | ||||
| 			/** @type {HTMLButtonElement} */(e.currentTarget).dataset.type); | ||||
| 		selectedDataType.set(type); | ||||
| 	}); | ||||
|  | ||||
| 	// Simulate data loading | ||||
| 	function loadData() { | ||||
| 		isLoading.set(true); | ||||
| 		error.set(null); | ||||
|  | ||||
| 		// Simulate API call | ||||
| 		setTimeout(() => { | ||||
| 			if (Math.random() > 0.9) { | ||||
| 				// Simulate occasional error | ||||
| 				error.set('Failed to load data. Please try again.'); | ||||
| 			} | ||||
| 			isLoading.set(false); | ||||
| 		}, 800); | ||||
| 	} | ||||
|  | ||||
| 	// Reactive chart rendering | ||||
| 	const chart = S(()=> { | ||||
| 		const chart= el("canvas", { id: "chart-canvas", width: 800, height: 400 }); | ||||
| 		const ctx = chart.getContext('2d'); | ||||
| 		const data = selectedData.get(); | ||||
| 		const months = DATA.months; | ||||
| 		const width = chart.width; | ||||
| 		const height = chart.height; | ||||
| 		const maxValue = Math.max(...data) * 1.1; | ||||
| 		const barWidth = width / data.length - 10; | ||||
|  | ||||
| 		// Clear canvas | ||||
| 		ctx.clearRect(0, 0, width, height); | ||||
|  | ||||
| 		// Draw background grid | ||||
| 		ctx.beginPath(); | ||||
| 		ctx.strokeStyle = '#f0f0f0'; | ||||
| 		ctx.lineWidth = 1; | ||||
| 		for(let i = 0; i < 5; i++) { | ||||
| 			const y = height - (height * (i / 5)) - 30; | ||||
| 			ctx.moveTo(50, y); | ||||
| 			ctx.lineTo(width - 20, y); | ||||
|  | ||||
| 			// Draw grid labels | ||||
| 			ctx.fillStyle = '#999'; | ||||
| 			ctx.font = '12px Arial'; | ||||
| 			ctx.fillText(Math.round(maxValue * (i / 5)), 20, y + 5); | ||||
| 		} | ||||
| 		ctx.stroke(); | ||||
|  | ||||
| 		// Draw bars | ||||
| 		data.forEach((value, index) => { | ||||
| 			const x = index * (barWidth + 10) + 60; | ||||
| 			const barHeight = (value / maxValue) * (height - 60); | ||||
|  | ||||
| 			// Bar | ||||
| 			ctx.fillStyle = '#4a90e2'; | ||||
| 			ctx.fillRect(x, height - barHeight - 30, barWidth, barHeight); | ||||
|  | ||||
| 			// Month label | ||||
| 			ctx.fillStyle = '#666'; | ||||
| 			ctx.font = '12px Arial'; | ||||
| 			ctx.fillText(months[index], x + barWidth/2 - 10, height - 10); | ||||
| 		}); | ||||
|  | ||||
| 		// Chart title | ||||
| 		ctx.fillStyle = '#333'; | ||||
| 		ctx.font = 'bold 14px Arial'; | ||||
| 		ctx.fillText(`${currentDataType.get().label} (${selectedYear.get()})`, width/2 - 80, 20); | ||||
| 		return chart; | ||||
| 	}); | ||||
|  | ||||
| 	return el("div", { className: "dashboard" }).append( | ||||
| 		el("header", { className: "dashboard-header" }).append( | ||||
| 			el("h1", "Sales Performance Dashboard"), | ||||
| 			el("div", { className: "year-filter" }).append( | ||||
| 				el("label", { htmlFor: "yearSelect", textContent: "Select Year:" }), | ||||
| 				el("select", { id: "yearSelect" }, | ||||
| 					on.host(el=> el.value = selectedYear.get().toString()), | ||||
| 					onYearChange | ||||
| 				).append( | ||||
| 					...years.map(year => el("option", { value: year, textContent: year })) | ||||
| 				) | ||||
| 			) | ||||
| 		), | ||||
|  | ||||
| 		// Error message (only shown when there's an error) | ||||
| 		S.el(error, errorMsg => !errorMsg | ||||
| 			? el() | ||||
| 			: el("div", { className: "error-message" }).append( | ||||
| 					el("p", errorMsg), | ||||
| 					el("button", { textContent: "Retry", type: "button" }, on("click", loadData)), | ||||
| 				), | ||||
| 		), | ||||
|  | ||||
| 		// Loading indicator | ||||
| 		S.el(isLoading, loading => !loading | ||||
| 			? el() | ||||
| 			: el("div", { className: "loading-spinner" }) | ||||
| 		), | ||||
|  | ||||
| 		// Main dashboard content | ||||
| 		el("div", { className: "dashboard-content" }).append( | ||||
| 			// Metrics cards | ||||
| 			el("div", { className: "metrics-container" }).append( | ||||
| 				el("div", { className: "metric-card" }).append( | ||||
| 					el("h3", "Total"), | ||||
| 					el("#text", S(() => `${totalValue.get().toLocaleString()}${currentDataType.get().unit}`)), | ||||
| 				), | ||||
| 				el("div", { className: "metric-card" }).append( | ||||
| 					el("h3", "Average"), | ||||
| 					el("#text", S(() => `${averageValue.get().toFixed(1)}${currentDataType.get().unit}`)), | ||||
| 				), | ||||
| 				el("div", { className: "metric-card" }).append( | ||||
| 					el("h3", "Highest"), | ||||
| 					el("#text", S(() => `${highestValue.get()}${currentDataType.get().unit}`)), | ||||
| 				), | ||||
| 			), | ||||
|  | ||||
| 			// Data type selection tabs | ||||
| 			el("div", { className: "data-type-tabs" }).append( | ||||
| 				...dataTypes.map(type => | ||||
| 					el("button", { | ||||
| 						type: "button", | ||||
| 						className: S(() => selectedDataType.get() === type.id ? 'active' : ''), | ||||
| 						dataType: type.id, | ||||
| 						textContent: type.label | ||||
| 					}, onDataTypeChange) | ||||
| 				) | ||||
| 			), | ||||
|  | ||||
| 			// Chart container | ||||
| 			el("div", { className: "chart-container" }).append( | ||||
| 				S.el(chart, chart => chart) | ||||
| 			) | ||||
| 		), | ||||
| 	); | ||||
| } | ||||
|  | ||||
| // Render the component | ||||
| document.body.append( | ||||
| 	el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append( | ||||
| 		el(DataDashboard) | ||||
| 	), | ||||
| 	el("style", ` | ||||
| 			.dashboard { | ||||
| 				font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||||
| 				max-width: 1000px; | ||||
| 				margin: 0 auto; | ||||
| 				padding: 1rem; | ||||
| 				background: #fff; | ||||
| 				box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | ||||
| 				border-radius: 8px; | ||||
| 			} | ||||
|  | ||||
| 			.dashboard-header { | ||||
| 				display: flex; | ||||
| 				justify-content: space-between; | ||||
| 				align-items: center; | ||||
| 				margin-bottom: 1.5rem; | ||||
| 				padding-bottom: 1rem; | ||||
| 				border-bottom: 1px solid #eee; | ||||
| 			} | ||||
|  | ||||
| 			.dashboard-header h1 { | ||||
| 				font-size: 1.5rem; | ||||
| 				margin: 0; | ||||
| 				color: #333; | ||||
| 			} | ||||
|  | ||||
| 			.year-filter { | ||||
| 				display: flex; | ||||
| 				align-items: center; | ||||
| 				gap: 0.5rem; | ||||
| 			} | ||||
|  | ||||
| 			.year-filter select { | ||||
| 				padding: 0.5rem; | ||||
| 				border: 1px solid #ddd; | ||||
| 				border-radius: 4px; | ||||
| 				font-size: 1rem; | ||||
| 			} | ||||
|  | ||||
| 			.metrics-container { | ||||
| 				display: grid; | ||||
| 				grid-template-columns: repeat(3, 1fr); | ||||
| 				gap: 1rem; | ||||
| 				margin-bottom: 1.5rem; | ||||
| 			} | ||||
|  | ||||
| 			.metric-card { | ||||
| 				background: #f9f9f9; | ||||
| 				border-radius: 8px; | ||||
| 				padding: 1rem; | ||||
| 				text-align: center; | ||||
| 				transition: transform 0.2s ease; | ||||
| 			} | ||||
|  | ||||
| 			.metric-card:hover { | ||||
| 				transform: translateY(-5px); | ||||
| 				box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | ||||
| 			} | ||||
|  | ||||
| 			.metric-card h3 { | ||||
| 				margin-top: 0; | ||||
| 				color: #666; | ||||
| 				font-size: 0.9rem; | ||||
| 				margin-bottom: 0.5rem; | ||||
| 			} | ||||
|  | ||||
| 			.metric-card p { | ||||
| 				font-size: 1.5rem; | ||||
| 				font-weight: bold; | ||||
| 				color: #333; | ||||
| 				margin: 0; | ||||
| 			} | ||||
|  | ||||
| 			.data-type-tabs { | ||||
| 				display: flex; | ||||
| 				border-bottom: 1px solid #eee; | ||||
| 				margin-bottom: 1.5rem; | ||||
| 			} | ||||
|  | ||||
| 			.data-type-tabs button { | ||||
| 				background: none; | ||||
| 				border: none; | ||||
| 				padding: 0.75rem 1.5rem; | ||||
| 				font-size: 1rem; | ||||
| 				cursor: pointer; | ||||
| 				color: #666; | ||||
| 				position: relative; | ||||
| 			} | ||||
|  | ||||
| 			.data-type-tabs button.active { | ||||
| 				color: #4a90e2; | ||||
| 				font-weight: 500; | ||||
| 			} | ||||
|  | ||||
| 			.data-type-tabs button.active::after { | ||||
| 				content: ''; | ||||
| 				position: absolute; | ||||
| 				bottom: -1px; | ||||
| 				left: 0; | ||||
| 				width: 100%; | ||||
| 				height: 3px; | ||||
| 				background: #4a90e2; | ||||
| 				border-radius: 3px 3px 0 0; | ||||
| 			} | ||||
|  | ||||
| 			.chart-container { | ||||
| 				background: #fff; | ||||
| 				border-radius: 8px; | ||||
| 				padding: 1rem; | ||||
| 				margin-bottom: 1.5rem; | ||||
| 				box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); | ||||
| 			} | ||||
|  | ||||
| 			.loading-spinner { | ||||
| 				display: flex; | ||||
| 				justify-content: center; | ||||
| 				align-items: center; | ||||
| 				height: 100px; | ||||
| 			} | ||||
|  | ||||
| 			.loading-spinner::before { | ||||
| 				content: ''; | ||||
| 				width: 40px; | ||||
| 				height: 40px; | ||||
| 				border: 4px solid #f3f3f3; | ||||
| 				border-top: 4px solid #4a90e2; | ||||
| 				border-radius: 50%; | ||||
| 				animation: spin 1s linear infinite; | ||||
| 			} | ||||
|  | ||||
| 			@keyframes spin { | ||||
| 				0% { transform: rotate(0deg); } | ||||
| 				100% { transform: rotate(360deg); } | ||||
| 			} | ||||
|  | ||||
| 			.error-message { | ||||
| 				background: #ffecec; | ||||
| 				color: #e74c3c; | ||||
| 				padding: 1rem; | ||||
| 				border-radius: 4px; | ||||
| 				margin-bottom: 1.5rem; | ||||
| 				display: flex; | ||||
| 				justify-content: space-between; | ||||
| 				align-items: center; | ||||
| 			} | ||||
|  | ||||
| 			.error-message p { | ||||
| 				margin: 0; | ||||
| 			} | ||||
|  | ||||
| 			.error-message button { | ||||
| 				background: #e74c3c; | ||||
| 				color: white; | ||||
| 				border: none; | ||||
| 				border-radius: 4px; | ||||
| 				padding: 0.5rem 1rem; | ||||
| 				cursor: pointer; | ||||
| 			} | ||||
|  | ||||
| 			@media (max-width: 768px) { | ||||
| 				.metrics-container { | ||||
| 					grid-template-columns: 1fr; | ||||
| 				} | ||||
|  | ||||
| 				.dashboard-header { | ||||
| 					flex-direction: column; | ||||
| 					align-items: flex-start; | ||||
| 					gap: 1rem; | ||||
| 				} | ||||
|  | ||||
| 				.year-filter { | ||||
| 					width: 100%; | ||||
| 				} | ||||
|  | ||||
| 				.year-filter select { | ||||
| 					flex-grow: 1; | ||||
| 				} | ||||
| 			} | ||||
| 		`) | ||||
| ); | ||||
							
								
								
									
										417
									
								
								docs/components/examples/case-studies/image-gallery.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								docs/components/examples/case-studies/image-gallery.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,417 @@ | ||||
| /** | ||||
|  * Case Study: Interactive Image Gallery | ||||
|  * | ||||
|  * This example demonstrates: | ||||
|  * - Dynamic loading of content | ||||
|  * - Lightbox functionality | ||||
|  * - Animation handling | ||||
|  * - Keyboard and gesture navigation | ||||
|  */ | ||||
|  | ||||
| import { el, memo, on } from "deka-dom-el"; | ||||
| import { S } from "deka-dom-el/signals"; | ||||
|  | ||||
| /** | ||||
|  * Interactive Image Gallery Component | ||||
|  * @returns {HTMLElement} Gallery element | ||||
|  */ | ||||
| export function ImageGallery() { | ||||
| 	// Sample image data | ||||
| 	const images = [ | ||||
| 		{ id: 1, src: 'https://api.algobook.info/v1/randomimage?category=nature', alt: 'Nature', title: 'Beautiful Landscape' }, | ||||
| 		{ id: 2, src: 'https://api.algobook.info/v1/randomimage?category=places', alt: 'City', title: 'Urban Architecture' }, | ||||
| 		{ id: 3, src: 'https://api.algobook.info/v1/randomimage?category=people', alt: 'People', title: 'Street Photography' }, | ||||
| 		{ id: 4, src: 'https://api.algobook.info/v1/randomimage?category=food', alt: 'Food', title: 'Culinary Delights' }, | ||||
| 		{ id: 5, src: 'https://api.algobook.info/v1/randomimage?category=animals', alt: 'Animals', title: 'Wildlife' }, | ||||
| 		{ id: 6, src: 'https://api.algobook.info/v1/randomimage?category=travel', alt: 'Travel', title: 'Adventure Awaits' }, | ||||
| 		{ id: 7, src: 'https://api.algobook.info/v1/randomimage?category=computer', alt: 'Technology', title: 'Modern Tech' }, | ||||
| 		{ id: 8, src: 'https://api.algobook.info/v1/randomimage?category=music', alt: 'Art', title: 'Creative Expression' }, | ||||
| 	]; | ||||
|  | ||||
| 	// Application state | ||||
| 	const selectedImageId = S(null); | ||||
| 	const filterTag = S('all'); | ||||
| 	const imagesToDisplay = S(() => { | ||||
| 		const tag = filterTag.get(); | ||||
| 		if (tag === 'all') return images; | ||||
| 		else return images.filter(img => img.alt.toLowerCase() === tag); | ||||
| 	}) | ||||
|  | ||||
| 	// Derived state | ||||
| 	const selectedImage = S(() => { | ||||
| 		const id = selectedImageId.get(); | ||||
| 		return id ? images.find(img => img.id === id) : null; | ||||
| 	}); | ||||
|  | ||||
| 	const isLightboxOpen = S(() => selectedImage.get() !== null); | ||||
|  | ||||
| 	// Event handlers | ||||
| 	const onImageClick = id => on("click", () => { | ||||
| 		selectedImageId.set(id); | ||||
| 		document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open | ||||
|  | ||||
| 		// Add keyboard event listeners when lightbox opens | ||||
| 		document.addEventListener('keydown', handleKeyDown); | ||||
| 	}); | ||||
| 	const closeLightbox = () => { | ||||
| 		selectedImageId.set(null); | ||||
| 		document.body.style.overflow = ''; // Restore scrolling | ||||
|  | ||||
| 		// Remove keyboard event listeners when lightbox closes | ||||
| 		document.removeEventListener('keydown', handleKeyDown); | ||||
| 	}; | ||||
| 	const onPrevImage = e => { | ||||
| 		e.stopPropagation(); // Prevent closing the lightbox | ||||
| 		const images = imagesToDisplay.get(); | ||||
| 		const currentId = selectedImageId.get(); | ||||
| 		const currentIndex = images.findIndex(img => img.id === currentId); | ||||
| 		const prevIndex = (currentIndex - 1 + images.length) % images.length; | ||||
| 		selectedImageId.set(images[prevIndex].id); | ||||
| 	}; | ||||
| 	const onNextImage = e => { | ||||
| 		e.stopPropagation(); // Prevent closing the lightbox | ||||
| 		const images = imagesToDisplay.get(); | ||||
| 		const currentId = selectedImageId.get(); | ||||
| 		const currentIndex = images.findIndex(img => img.id === currentId); | ||||
| 		const nextIndex = (currentIndex + 1) % images.length; | ||||
| 		selectedImageId.set(images[nextIndex].id); | ||||
| 	}; | ||||
| 	const onFilterChange = tag => on("click", () => { | ||||
| 		filterTag.set(tag); | ||||
| 	}); | ||||
|  | ||||
| 	// Keyboard navigation handler | ||||
| 	function handleKeyDown(e) { | ||||
| 		switch(e.key) { | ||||
| 			case 'Escape': | ||||
| 				closeLightbox(); | ||||
| 				break; | ||||
| 			case 'ArrowLeft': | ||||
| 				document.querySelector('.lightbox-prev-btn').click(); | ||||
| 				break; | ||||
| 			case 'ArrowRight': | ||||
| 				document.querySelector('.lightbox-next-btn').click(); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Build the gallery UI | ||||
| 	return el("div", { className: "gallery-container" }).append( | ||||
| 		// Gallery header | ||||
| 		el("header", { className: "gallery-header" }).append( | ||||
| 			el("h1", "Interactive Image Gallery"), | ||||
| 			el("p", "Click on any image to view it in the lightbox. Use arrow keys for navigation.") | ||||
| 		), | ||||
|  | ||||
| 		// Filter options | ||||
| 		el("div", { className: "gallery-filters" }).append( | ||||
| 			el("button", { | ||||
| 				classList: { active: S(() => filterTag.get() === 'all') }, | ||||
| 				textContent: "All" | ||||
| 			}, onFilterChange('all')), | ||||
| 			el("button", { | ||||
| 				classList: { active: S(() => filterTag.get() === 'nature') }, | ||||
| 				textContent: "Nature" | ||||
| 			}, onFilterChange('nature')), | ||||
| 			el("button", { | ||||
| 				classList: { active: S(() => filterTag.get() === 'urban') }, | ||||
| 				textContent: "Urban" | ||||
| 			}, onFilterChange('urban')), | ||||
| 			el("button", { | ||||
| 				classList: { active: S(() => filterTag.get() === 'people') }, | ||||
| 				textContent: "People" | ||||
| 			}, onFilterChange('people')) | ||||
| 		), | ||||
|  | ||||
| 		// Image grid | ||||
| 		el("div", { className: "gallery-grid" }).append( | ||||
| 			S.el(imagesToDisplay, images => | ||||
| 				images.map(image => | ||||
| 					memo(image.id, ()=> | ||||
| 						el("div", { | ||||
| 							className: "gallery-item", | ||||
| 							dataTag: image.alt.toLowerCase() | ||||
| 						}).append( | ||||
| 							el("img", { | ||||
| 								src: image.src, | ||||
| 								alt: image.alt, | ||||
| 								loading: "lazy" | ||||
| 							}, onImageClick(image.id)), | ||||
| 							el("div", { className: "gallery-item-caption" }).append( | ||||
| 								el("h3", image.title), | ||||
| 								el("p", image.alt) | ||||
| 							) | ||||
| 						) | ||||
| 					) | ||||
| 				) | ||||
| 			) | ||||
| 		), | ||||
|  | ||||
| 		// Lightbox (only shown when an image is selected) | ||||
| 		S.el(isLightboxOpen, open => !open | ||||
| 			? el() | ||||
| 			: el("div", { className: "lightbox-overlay" }, on("click", closeLightbox)).append( | ||||
| 					el("div", { | ||||
| 						className: "lightbox-content", | ||||
| 						onClick: e => e.stopPropagation() // Prevent closing when clicking inside | ||||
| 					}).append( | ||||
| 						el("button", { | ||||
| 							className: "lightbox-close-btn", | ||||
| 							"aria-label": "Close lightbox" | ||||
| 						}, on("click", closeLightbox)).append("×"), | ||||
|  | ||||
| 						el("button", { | ||||
| 							className: "lightbox-prev-btn", | ||||
| 							"aria-label": "Previous image" | ||||
| 						}, on("click", onPrevImage)).append("❮"), | ||||
|  | ||||
| 						el("button", { | ||||
| 							className: "lightbox-next-btn", | ||||
| 							"aria-label": "Next image" | ||||
| 						}, on("click", onNextImage)).append("❯"), | ||||
|  | ||||
| 						S.el(selectedImage, img => !img | ||||
| 							? el() | ||||
| 							: el("div", { className: "lightbox-image-container" }).append( | ||||
| 								el("img", { | ||||
| 									src: img.src, | ||||
| 									alt: img.alt, | ||||
| 									className: "lightbox-image" | ||||
| 								}), | ||||
| 								el("div", { className: "lightbox-caption" }).append( | ||||
| 									el("h2", img.title), | ||||
| 									el("p", img.alt) | ||||
| 								) | ||||
| 							) | ||||
| 						) | ||||
| 					) | ||||
| 				) | ||||
| 		), | ||||
| 	); | ||||
| } | ||||
|  | ||||
| // Render the component | ||||
| document.body.append( | ||||
| 	el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append( | ||||
| 		el(ImageGallery) | ||||
| 	), | ||||
| 	el("style", ` | ||||
| 		.gallery-container { | ||||
| 			font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||||
| 			max-width: 1200px; | ||||
| 			margin: 0 auto; | ||||
| 			padding: 2rem; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-header { | ||||
| 			text-align: center; | ||||
| 			margin-bottom: 2rem; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-header h1 { | ||||
| 			margin-bottom: 0.5rem; | ||||
| 			color: #333; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-header p { | ||||
| 			color: #666; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-filters { | ||||
| 			display: flex; | ||||
| 			justify-content: center; | ||||
| 			margin-bottom: 2rem; | ||||
| 			flex-wrap: wrap; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-filters button { | ||||
| 			background: none; | ||||
| 			border: none; | ||||
| 			padding: 0.5rem 1.5rem; | ||||
| 			margin: 0 0.5rem; | ||||
| 			font-size: 1rem; | ||||
| 			cursor: pointer; | ||||
| 			border-radius: 30px; | ||||
| 			transition: all 0.3s ease; | ||||
| 			color: #555; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-filters button:hover { | ||||
| 			background: #f0f0f0; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-filters button.active { | ||||
| 			background: #4a90e2; | ||||
| 			color: white; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-grid { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | ||||
| 			gap: 1.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item { | ||||
| 			position: relative; | ||||
| 			border-radius: 8px; | ||||
| 			overflow: hidden; | ||||
| 			box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); | ||||
| 			transition: transform 0.3s ease, box-shadow 0.3s ease; | ||||
| 			cursor: pointer; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item:hover { | ||||
| 			transform: translateY(-5px); | ||||
| 			box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item img { | ||||
| 			width: 100%; | ||||
| 			height: 200px; | ||||
| 			object-fit: cover; | ||||
| 			display: block; | ||||
| 			transition: transform 0.5s ease; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item:hover img { | ||||
| 			transform: scale(1.05); | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item-caption { | ||||
| 			position: absolute; | ||||
| 			bottom: 0; | ||||
| 			left: 0; | ||||
| 			right: 0; | ||||
| 			background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); | ||||
| 			color: white; | ||||
| 			padding: 1rem; | ||||
| 			transform: translateY(100%); | ||||
| 			transition: transform 0.3s ease; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item:hover .gallery-item-caption { | ||||
| 			transform: translateY(0); | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item-caption h3 { | ||||
| 			margin: 0 0 0.5rem; | ||||
| 			font-size: 1.2rem; | ||||
| 		} | ||||
|  | ||||
| 		.gallery-item-caption p { | ||||
| 			margin: 0; | ||||
| 			font-size: 0.9rem; | ||||
| 			opacity: 0.8; | ||||
| 		} | ||||
|  | ||||
| 		/* Lightbox styles */ | ||||
| 		.lightbox-overlay { | ||||
| 			position: fixed; | ||||
| 			top: 0; | ||||
| 			left: 0; | ||||
| 			right: 0; | ||||
| 			bottom: 0; | ||||
| 			background: rgba(0, 0, 0, 0.9); | ||||
| 			display: flex; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 			z-index: 1000; | ||||
| 			padding: 2rem; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-content { | ||||
| 			position: relative; | ||||
| 			max-width: 90%; | ||||
| 			max-height: 90%; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-image-container { | ||||
| 			overflow: hidden; | ||||
| 			border-radius: 4px; | ||||
| 			box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); | ||||
| 			background: #000; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-image { | ||||
| 			max-width: 100%; | ||||
| 			max-height: 80vh; | ||||
| 			display: block; | ||||
| 			margin: 0 auto; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-caption { | ||||
| 			background: #222; | ||||
| 			color: white; | ||||
| 			padding: 1rem; | ||||
| 			text-align: center; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-caption h2 { | ||||
| 			margin: 0 0 0.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-caption p { | ||||
| 			margin: 0; | ||||
| 			opacity: 0.8; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-close-btn, | ||||
| 		.lightbox-prev-btn, | ||||
| 		.lightbox-next-btn { | ||||
| 			background: rgba(0, 0, 0, 0.5); | ||||
| 			color: white; | ||||
| 			border: none; | ||||
| 			font-size: 1.5rem; | ||||
| 			width: 50px; | ||||
| 			height: 50px; | ||||
| 			border-radius: 50%; | ||||
| 			display: flex; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 			cursor: pointer; | ||||
| 			transition: background 0.3s ease; | ||||
| 			position: absolute; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-close-btn:hover, | ||||
| 		.lightbox-prev-btn:hover, | ||||
| 		.lightbox-next-btn:hover { | ||||
| 			background: rgba(0, 0, 0, 0.8); | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-close-btn { | ||||
| 			top: -25px; | ||||
| 			right: -25px; | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-prev-btn { | ||||
| 			left: -25px; | ||||
| 			top: 50%; | ||||
| 			transform: translateY(-50%); | ||||
| 		} | ||||
|  | ||||
| 		.lightbox-next-btn { | ||||
| 			right: -25px; | ||||
| 			top: 50%; | ||||
| 			transform: translateY(-50%); | ||||
| 		} | ||||
|  | ||||
| 		@media (max-width: 768px) { | ||||
| 			.gallery-container { | ||||
| 				padding: 1rem; | ||||
| 			} | ||||
|  | ||||
| 			.gallery-grid { | ||||
| 				grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | ||||
| 				gap: 1rem; | ||||
| 			} | ||||
|  | ||||
| 			.lightbox-prev-btn, | ||||
| 			.lightbox-next-btn { | ||||
| 				width: 40px; | ||||
| 				height: 40px; | ||||
| 				font-size: 1.2rem; | ||||
| 			} | ||||
| 		} | ||||
| 	`) | ||||
| ); | ||||
							
								
								
									
										342
									
								
								docs/components/examples/case-studies/interactive-form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										342
									
								
								docs/components/examples/case-studies/interactive-form.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,342 @@ | ||||
| /** | ||||
|  * Case Study: Interactive Form with Validation | ||||
|  * | ||||
|  * This example demonstrates: | ||||
|  * - Form handling with real-time validation | ||||
|  * - Reactive UI updates based on input state | ||||
|  * - Complex form state management | ||||
|  * - Clean separation of concerns (data, validation, UI) | ||||
|  */ | ||||
|  | ||||
| import { dispatchEvent, el, on, scope } from "deka-dom-el"; | ||||
| import { S } from "deka-dom-el/signals"; | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} FormState | ||||
|  * @property {string} name | ||||
|  * @property {string} email | ||||
|  * @property {string} password | ||||
|  * @property {string} confirmPassword | ||||
|  * @property {boolean} agreedToTerms | ||||
|  * */ | ||||
| /** | ||||
|  * Interactive Form with Validation Component | ||||
|  * @returns {HTMLElement} Form element | ||||
|  */ | ||||
| export function InteractiveForm() { | ||||
| 	const submitted = S(false); | ||||
| 	/** @type {FormState|null} */ | ||||
| 	let formState = null; | ||||
| 	/** @param {CustomEvent<FormState>} event */ | ||||
| 	const onSubmit = ({ detail }) => { | ||||
| 		submitted.set(true); | ||||
| 		formState = detail; | ||||
| 	}; | ||||
| 	const onAnotherAccount = () => { | ||||
| 		submitted.set(false) | ||||
| 		formState = null; | ||||
| 	}; | ||||
|  | ||||
| 	return el("div", { className: "form-container" }).append( | ||||
| 		S.el(submitted, s => s | ||||
| 			? el("div", { className: "success-message" }).append( | ||||
| 				el("h3", "Thank you for registering!"), | ||||
| 				el("p", `Welcome, ${formState.name}! Your account has been created successfully.`), | ||||
| 				el("button", { textContent: "Register another account", type: "button" }, | ||||
| 					on("click", onAnotherAccount) | ||||
| 				), | ||||
| 			) | ||||
| 			: el(Form, { initial: formState }, on("form:submit", onSubmit)) | ||||
| 		) | ||||
| 	); | ||||
| } | ||||
| /** | ||||
|  * Form Component | ||||
|  * @type {(props: { initial: FormState | null }) => HTMLElement} | ||||
|  * */ | ||||
| export function Form({ initial }) { | ||||
| 	const { host }= scope; | ||||
| 	// Form state management | ||||
| 	const formState = S(initial || { | ||||
| 		name: '', | ||||
| 		email: '', | ||||
| 		password: '', | ||||
| 		confirmPassword: '', | ||||
| 		agreedToTerms: false | ||||
| 	}, { | ||||
| 		/** | ||||
| 		 * @template {keyof FormState} K | ||||
| 		 * @param {K} key | ||||
| 		 * @param {FormState[K]} value | ||||
| 		 * */ | ||||
| 		update(key, value) { | ||||
| 			this.value[key] = value; | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	// Derived signals for validation | ||||
| 	const nameValid = S(() => formState.get().name.length >= 3); | ||||
| 	const emailValid = S(() => { | ||||
| 		const email = formState.get().email; | ||||
| 		return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); | ||||
| 	}); | ||||
| 	const passwordValid = S(() => { | ||||
| 		const password = formState.get().password; | ||||
| 		return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password); | ||||
| 	}); | ||||
| 	const passwordsMatch = S(() => { | ||||
| 		const { password, confirmPassword } = formState.get(); | ||||
| 		return password === confirmPassword && confirmPassword !== ''; | ||||
| 	}); | ||||
| 	const termsAgreed = S(() => formState.get().agreedToTerms); | ||||
|  | ||||
| 	// Overall form validity | ||||
| 	const formValid = S(() => | ||||
| 		nameValid.get() && | ||||
| 		emailValid.get() && | ||||
| 		passwordValid.get() && | ||||
| 		passwordsMatch.get() && | ||||
| 		termsAgreed.get() | ||||
| 	); | ||||
|  | ||||
| 	// Event handlers | ||||
| 	/** | ||||
| 	 * Event handler for input events | ||||
| 	 * @param {"value"|"checked"} prop | ||||
| 	 * @returns {(ev: Event) => void} | ||||
| 	 * */ | ||||
| 	const onChange= prop => ev => { | ||||
| 		const input = /** @type {HTMLInputElement} */(ev.target); | ||||
| 		S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]); | ||||
| 	}; | ||||
| 	const dispatcSubmit = dispatchEvent("form:submit", host); | ||||
| 	const onSubmit = on("submit", e => { | ||||
| 		e.preventDefault(); | ||||
| 		if (!formValid.get()) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		dispatcSubmit(formState.get()); | ||||
| 	}); | ||||
|  | ||||
| 	// Component UI | ||||
| 	return el("form", { className: "registration-form" }, onSubmit).append( | ||||
| 		el("h2", "Create an Account"), | ||||
|  | ||||
| 		// Name field | ||||
| 		el("div", { classList: { | ||||
| 			"form-group": true, | ||||
| 			valid: nameValid, | ||||
| 			invalid: S(()=> !nameValid.get() && formState.get().name) | ||||
| 		}}).append( | ||||
| 			el("label", { htmlFor: "name", textContent: "Full Name" }), | ||||
| 			el("input", { | ||||
| 				id: "name", | ||||
| 				type: "text", | ||||
| 				value: formState.get().name, | ||||
| 				placeholder: "Enter your full name" | ||||
| 			}, on("input", onChange("value"))), | ||||
| 			el("div", { className: "validation-message", textContent: "Name must be at least 3 characters long" }), | ||||
| 		), | ||||
|  | ||||
| 		// Email field | ||||
| 		el("div", { classList: { | ||||
| 			"form-group": true, | ||||
| 			valid: emailValid, | ||||
| 			invalid: S(()=> !emailValid.get() && formState.get().email) | ||||
| 		}}).append( | ||||
| 			el("label", { htmlFor: "email", textContent: "Email Address" }), | ||||
| 			el("input", { | ||||
| 				id: "email", | ||||
| 				type: "email", | ||||
| 				value: formState.get().email, | ||||
| 				placeholder: "Enter your email address" | ||||
| 			}, on("input", onChange("value"))), | ||||
| 			el("div", { className: "validation-message", textContent: "Please enter a valid email address" }) | ||||
| 		), | ||||
|  | ||||
| 		// Password field | ||||
| 		el("div", { classList: { | ||||
| 			"form-group": true, | ||||
| 			valid: passwordValid, | ||||
| 			invalid: S(()=> !passwordValid.get() && formState.get().password) | ||||
| 		}}).append( | ||||
| 			el("label", { htmlFor: "password", textContent: "Password" }), | ||||
| 			el("input", { | ||||
| 				id: "password", | ||||
| 				type: "password", | ||||
| 				value: formState.get().password, | ||||
| 				placeholder: "Create a password" | ||||
| 			}, on("input", onChange("value"))), | ||||
| 			el("div", { | ||||
| 				className: "validation-message", | ||||
| 				textContent: "Password must be at least 8 characters with at least one uppercase letter and one number", | ||||
| 			}), | ||||
| 		), | ||||
|  | ||||
| 		// Confirm password field | ||||
| 		el("div", { classList: { | ||||
| 			"form-group": true, | ||||
| 			valid: passwordsMatch, | ||||
| 			invalid: S(()=> !passwordsMatch.get() && formState.get().confirmPassword) | ||||
| 		}}).append( | ||||
| 			el("label", { htmlFor: "confirmPassword", textContent: "Confirm Password" }), | ||||
| 			el("input", { | ||||
| 				id: "confirmPassword", | ||||
| 				type: "password", | ||||
| 				value: formState.get().confirmPassword, | ||||
| 				placeholder: "Confirm your password" | ||||
| 			}, on("input", onChange("value"))), | ||||
| 			el("div", { className: "validation-message", textContent: "Passwords must match" }), | ||||
| 		), | ||||
|  | ||||
| 		// Terms agreement | ||||
| 		el("div", { className: "form-group checkbox-group" }).append( | ||||
| 			el("input", { | ||||
| 				id: "agreedToTerms", | ||||
| 				type: "checkbox", | ||||
| 				checked: formState.get().agreedToTerms | ||||
| 			}, on("change", onChange("checked"))), | ||||
| 			el("label", { htmlFor: "agreedToTerms", textContent: "I agree to the Terms and Conditions" }), | ||||
| 		), | ||||
|  | ||||
| 		// Submit button | ||||
| 		el("button", { | ||||
| 			textContent: "Create Account", | ||||
| 			type: "submit", | ||||
| 			className: "submit-button", | ||||
| 			disabled: S(() => !formValid.get()) | ||||
| 		}), | ||||
| 	); | ||||
| } | ||||
|  | ||||
| // Render the component | ||||
| document.body.append( | ||||
| 	el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append( | ||||
| 		el(InteractiveForm) | ||||
| 	), | ||||
| 	el("style", ` | ||||
| 		.form-container { | ||||
| 			font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||||
| 			max-width: 500px; | ||||
| 			margin: 0 auto; | ||||
| 			padding: 2rem; | ||||
| 			border-radius: 8px; | ||||
| 			box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | ||||
| 			background: #fff; | ||||
| 		} | ||||
|  | ||||
| 		h2 { | ||||
| 			margin-top: 0; | ||||
| 			color: #333; | ||||
| 			margin-bottom: 1.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.form-group { | ||||
| 			margin-bottom: 1.5rem; | ||||
| 			position: relative; | ||||
| 			transition: all 0.3s ease; | ||||
| 		} | ||||
|  | ||||
| 		label { | ||||
| 			display: block; | ||||
| 			margin-bottom: 0.5rem; | ||||
| 			color: #555; | ||||
| 			font-weight: 500; | ||||
| 		} | ||||
|  | ||||
| 		input[type="text"], | ||||
| 		input[type="email"], | ||||
| 		input[type="password"] { | ||||
| 			width: 100%; | ||||
| 			padding: 0.75rem; | ||||
| 			border: 1px solid #ddd; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 1rem; | ||||
| 			transition: border-color 0.3s ease; | ||||
| 		} | ||||
|  | ||||
| 		input:focus { | ||||
| 			outline: none; | ||||
| 			border-color: #4a90e2; | ||||
| 			box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); | ||||
| 		} | ||||
|  | ||||
| 		.checkbox-group { | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 		} | ||||
|  | ||||
| 		.checkbox-group label { | ||||
| 			margin: 0 0 0 0.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.validation-message { | ||||
| 			font-size: 0.85rem; | ||||
| 			color: #e74c3c; | ||||
| 			margin-top: 0.5rem; | ||||
| 			height: 0; | ||||
| 			overflow: hidden; | ||||
| 			opacity: 0; | ||||
| 			transition: all 0.3s ease; | ||||
| 		} | ||||
|  | ||||
| 		.form-group.invalid .validation-message { | ||||
| 			height: auto; | ||||
| 			opacity: 1; | ||||
| 		} | ||||
|  | ||||
| 		.form-group.valid input { | ||||
| 			border-color: #2ecc71; | ||||
| 		} | ||||
|  | ||||
| 		.form-group.invalid input { | ||||
| 			border-color: #e74c3c; | ||||
| 		} | ||||
|  | ||||
| 		.submit-button { | ||||
| 			background-color: #4a90e2; | ||||
| 			color: white; | ||||
| 			border: none; | ||||
| 			border-radius: 4px; | ||||
| 			padding: 0.75rem 1.5rem; | ||||
| 			font-size: 1rem; | ||||
| 			cursor: pointer; | ||||
| 			transition: background-color 0.3s ease; | ||||
| 			width: 100%; | ||||
| 		} | ||||
|  | ||||
| 		.submit-button:hover:not(:disabled) { | ||||
| 			background-color: #3a7bc8; | ||||
| 		} | ||||
|  | ||||
| 		.submit-button:disabled { | ||||
| 			background-color: #b5b5b5; | ||||
| 			cursor: not-allowed; | ||||
| 		} | ||||
|  | ||||
| 		.success-message { | ||||
| 			text-align: center; | ||||
| 			color: #2ecc71; | ||||
| 		} | ||||
|  | ||||
| 		.success-message h3 { | ||||
| 			margin-top: 0; | ||||
| 		} | ||||
|  | ||||
| 		.success-message button { | ||||
| 			background-color: #2ecc71; | ||||
| 			color: white; | ||||
| 			border: none; | ||||
| 			border-radius: 4px; | ||||
| 			padding: 0.75rem 1.5rem; | ||||
| 			font-size: 1rem; | ||||
| 			cursor: pointer; | ||||
| 			margin-top: 1rem; | ||||
| 		} | ||||
|  | ||||
| 		.success-message button:hover { | ||||
| 			background-color: #27ae60; | ||||
| 		} | ||||
| 	`), | ||||
| ); | ||||
							
								
								
									
										717
									
								
								docs/components/examples/case-studies/task-manager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										717
									
								
								docs/components/examples/case-studies/task-manager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,717 @@ | ||||
| /** | ||||
|  * Case Study: Task Manager Application | ||||
|  * | ||||
|  * This example demonstrates: | ||||
|  * - Complex state management with signals | ||||
|  * - Drag and drop functionality | ||||
|  * - Local storage persistence | ||||
|  * - Responsive design for different devices | ||||
|  */ | ||||
|  | ||||
| import { el, on } from "deka-dom-el"; | ||||
| import { S } from "deka-dom-el/signals"; | ||||
|  | ||||
| /** | ||||
|  * Task Manager Component | ||||
|  * @returns {HTMLElement} Task manager UI | ||||
|  */ | ||||
| export function TaskManager() { | ||||
| 	// Local storage key | ||||
| 	const STORAGE_KEY = 'dde-task-manager'; | ||||
|  | ||||
| 	// Task statuses | ||||
| 	const STATUSES = { | ||||
| 		TODO: 'todo', | ||||
| 		IN_PROGRESS: 'in-progress', | ||||
| 		DONE: 'done' | ||||
| 	}; | ||||
|  | ||||
| 	// Initial tasks from localStorage or defaults | ||||
| 	let initialTasks = []; | ||||
| 	try { | ||||
| 		const saved = localStorage.getItem(STORAGE_KEY); | ||||
| 		if (saved) { | ||||
| 			initialTasks = JSON.parse(saved); | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		console.error('Failed to load tasks from localStorage', e); | ||||
| 	} | ||||
|  | ||||
| 	if (!initialTasks.length) { | ||||
| 		// Default tasks if nothing in localStorage | ||||
| 		initialTasks = [ | ||||
| 			{ id: 1, title: 'Create project structure', description: 'Set up folders and initial files', status: STATUSES.DONE, priority: 'high' }, | ||||
| 			{ id: 2, title: 'Design UI components', description: 'Create mockups for main views', status: STATUSES.IN_PROGRESS, priority: 'medium' }, | ||||
| 			{ id: 3, title: 'Implement authentication', description: 'Set up user login and registration', status: STATUSES.TODO, priority: 'high' }, | ||||
| 			{ id: 4, title: 'Write documentation', description: 'Document API endpoints and usage examples', status: STATUSES.TODO, priority: 'low' }, | ||||
| 		]; | ||||
| 	} | ||||
|  | ||||
| 	// Application state | ||||
| 	const tasks = S(initialTasks, { | ||||
| 		add(task) { this.value.push(task); }, | ||||
| 		remove(id) { this.value = this.value.filter(task => task.id !== id); }, | ||||
| 		update(id, task) { | ||||
| 			const current= this.value.find(t => t.id === id); | ||||
| 			if (current) Object.assign(current, task); | ||||
| 			// TODO: known bug for derived signals (the object is the same) | ||||
| 			// so filteredTasks is not updated, hotfix ↙ | ||||
| 			this.value = structuredClone(this.value); | ||||
| 		} | ||||
| 	}); | ||||
| 	const newTaskTitle = S(''); | ||||
| 	const newTaskDescription = S(''); | ||||
| 	const newTaskPriority = S('medium'); | ||||
| 	const editingTaskId = S(null); | ||||
| 	const draggedTaskId = S(null); | ||||
| 	const filterPriority = S('all'); | ||||
| 	const searchQuery = S(''); | ||||
|  | ||||
| 	// Save tasks to localStorage whenever they change | ||||
| 	S.on(tasks, value => { | ||||
| 		try { | ||||
| 			localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); | ||||
| 		} catch (e) { | ||||
| 			console.error('Failed to save tasks to localStorage', e); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	// Filtered tasks based on priority and search query | ||||
| 	const filteredTasks = S(() => { | ||||
| 		let filtered = tasks.get(); | ||||
|  | ||||
| 		// Filter by priority | ||||
| 		if (filterPriority.get() !== 'all') { | ||||
| 			filtered = filtered.filter(task => task.priority === filterPriority.get()); | ||||
| 		} | ||||
|  | ||||
| 		// Filter by search query | ||||
| 		const query = searchQuery.get().toLowerCase(); | ||||
| 		if (query) { | ||||
| 			filtered = filtered.filter(task => | ||||
| 				task.title.toLowerCase().includes(query) || | ||||
| 				task.description.toLowerCase().includes(query) | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		return filtered; | ||||
| 	}); | ||||
|  | ||||
| 	// Tasks grouped by status for display in columns | ||||
| 	const tasksByStatus = S(() => { | ||||
| 		const filtered = filteredTasks.get(); | ||||
| 		return { | ||||
| 			[STATUSES.TODO]: filtered.filter(t => t.status === STATUSES.TODO), | ||||
| 			[STATUSES.IN_PROGRESS]: filtered.filter(t => t.status === STATUSES.IN_PROGRESS), | ||||
| 			[STATUSES.DONE]: filtered.filter(t => t.status === STATUSES.DONE) | ||||
| 		}; | ||||
| 	}); | ||||
|  | ||||
| 	// Event handlers | ||||
| 	const onAddTask = e => { | ||||
| 		e.preventDefault(); | ||||
|  | ||||
| 		const title = newTaskTitle.get().trim(); | ||||
| 		if (!title) return; | ||||
|  | ||||
| 		const newTask = { | ||||
| 			id: Date.now(), | ||||
| 			title, | ||||
| 			description: newTaskDescription.get().trim(), | ||||
| 			status: STATUSES.TODO, | ||||
| 			priority: newTaskPriority.get() | ||||
| 		}; | ||||
|  | ||||
| 		S.action(tasks, "add", newTask); | ||||
| 		newTaskTitle.set(''); | ||||
| 		newTaskDescription.set(''); | ||||
| 		newTaskPriority.set('medium'); | ||||
| 	}; | ||||
| 	const onDeleteTask = id => e => { | ||||
| 		e.stopPropagation(); | ||||
| 		S.action(tasks, "remove", id); | ||||
| 	}; | ||||
| 	const onEditStart = id => () => { | ||||
| 		editingTaskId.set(id); | ||||
| 	}; | ||||
| 	const onEditCancel = () => { | ||||
| 		editingTaskId.set(null); | ||||
| 	}; | ||||
| 	const onEditSave = id => e => { | ||||
| 		e.preventDefault(); | ||||
| 		const form = e.target; | ||||
| 		const title = form.elements.title.value.trim(); | ||||
|  | ||||
| 		if (!title) return; | ||||
|  | ||||
| 		tasks.set(tasks.get().map(task => | ||||
| 			task.id === id | ||||
| 				? { | ||||
| 						...task, | ||||
| 						title, | ||||
| 						description: form.elements.description.value.trim(), | ||||
| 						priority: form.elements.priority.value | ||||
| 					} | ||||
| 				: task | ||||
| 		)); | ||||
|  | ||||
| 		editingTaskId.set(null); | ||||
| 	}; | ||||
|  | ||||
| 	function onDragable(id) { | ||||
| 		return element => { | ||||
| 			on("dragstart", e => { | ||||
| 				draggedTaskId.set(id); | ||||
| 				e.dataTransfer.effectAllowed = 'move'; | ||||
|  | ||||
| 				// Add some styling to the element being dragged | ||||
| 				setTimeout(() => { | ||||
| 					const el = document.getElementById(`task-${id}`); | ||||
| 					if (el) el.classList.add('dragging'); | ||||
| 				}, 0); | ||||
| 			})(element); | ||||
|  | ||||
| 			on("dragend", () => { | ||||
| 				draggedTaskId.set(null); | ||||
|  | ||||
| 				// Remove the styling | ||||
| 				const el = document.getElementById(`task-${id}`); | ||||
| 				if (el) el.classList.remove('dragging'); | ||||
| 			})(element); | ||||
| 		}; | ||||
| 	} | ||||
| 	function onDragArea(status) { | ||||
| 		return element => { | ||||
| 			on("dragover", e => { | ||||
| 				e.preventDefault(); | ||||
| 				e.dataTransfer.dropEffect = 'move'; | ||||
|  | ||||
| 				// Add a visual indicator for the drop target | ||||
| 				const column = document.getElementById(`column-${status}`); | ||||
| 				if (column) column.classList.add('drag-over'); | ||||
| 			})(element); | ||||
|  | ||||
| 			on("dragleave", () => { | ||||
| 				// Remove the visual indicator | ||||
| 				const column = document.getElementById(`column-${status}`); | ||||
| 				if (column) column.classList.remove('drag-over'); | ||||
| 			})(element); | ||||
|  | ||||
| 			on("drop", e => { | ||||
| 				e.preventDefault(); | ||||
| 				const id = draggedTaskId.get(); | ||||
| 				if (id) S.action(tasks, "update", id, { status }); | ||||
| 				// Remove the visual indicator | ||||
| 				const column = document.getElementById(`column-${status}`); | ||||
| 				if (column) column.classList.remove('drag-over'); | ||||
| 			})(element); | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	// Helper function to render a task card | ||||
| 	function renderTaskCard(task) { | ||||
| 		const isEditing = S(() => editingTaskId.get() === task.id); | ||||
|  | ||||
| 		return el("div", { | ||||
| 			id: `task-${task.id}`, | ||||
| 			className: `task-card priority-${task.priority}`, | ||||
| 			draggable: true | ||||
| 		}, onDragable(task.id)).append( | ||||
| 			S.el(isEditing, editing => editing | ||||
| 				// Edit mode | ||||
| 				? el("form", { className: "task-edit-form" }, on("submit", onEditSave(task.id))).append( | ||||
| 						el("input", { | ||||
| 							name: "title", | ||||
| 							className: "task-title-input", | ||||
| 							defaultValue: task.title, | ||||
| 							placeholder: "Task title", | ||||
| 							required: true, | ||||
| 							autoFocus: true | ||||
| 						}), | ||||
| 						el("textarea", { | ||||
| 							name: "description", | ||||
| 							className: "task-desc-input", | ||||
| 							defaultValue: task.description, | ||||
| 							placeholder: "Description (optional)" | ||||
| 						}), | ||||
| 						el("select", { | ||||
| 							name: "priority", | ||||
| 						}, on.host(el=> el.value = task.priority)).append( | ||||
| 							el("option", { value: "low", textContent: "Low Priority" }), | ||||
| 							el("option", { value: "medium", textContent: "Medium Priority" }), | ||||
| 							el("option", { value: "high", textContent: "High Priority" }) | ||||
| 						), | ||||
| 						el("div", { className: "task-edit-actions" }).append( | ||||
| 							el("button", { | ||||
| 								type: "button", | ||||
| 								className: "cancel-btn" | ||||
| 							}, on("click", onEditCancel)).append("Cancel"), | ||||
| 							el("button", { | ||||
| 								type: "submit", | ||||
| 								className: "save-btn" | ||||
| 							}).append("Save") | ||||
| 						) | ||||
| 					) | ||||
| 				// View mode | ||||
| 				: el().append( | ||||
| 						el("div", { className: "task-header" }).append( | ||||
| 							el("h3", { className: "task-title", textContent: task.title }), | ||||
| 							el("div", { className: "task-actions" }).append( | ||||
| 								el("button", { | ||||
| 									className: "edit-btn", | ||||
| 									"aria-label": "Edit task" | ||||
| 								}, on("click", onEditStart(task.id))).append("✎"), | ||||
| 								el("button", { | ||||
| 									className: "delete-btn", | ||||
| 									"aria-label": "Delete task" | ||||
| 								}, on("click", onDeleteTask(task.id))).append("×") | ||||
| 							) | ||||
| 						), | ||||
| 						task.description | ||||
| 							? el("p", { className: "task-description", textContent: task.description }) | ||||
| 							: el(), | ||||
| 						el("div", { className: "task-meta" }).append( | ||||
| 							el("span", { | ||||
| 								className: `priority-badge priority-${task.priority}`, | ||||
| 								textContent: task.priority.charAt(0).toUpperCase() + task.priority.slice(1) | ||||
| 							}) | ||||
| 						) | ||||
| 					) | ||||
| 			) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	// Build the task manager UI | ||||
| 	return el("div", { className: "task-manager" }).append( | ||||
| 		el("header", { className: "app-header" }).append( | ||||
| 			el("h1", "DDE Task Manager"), | ||||
| 			el("div", { className: "app-controls" }).append( | ||||
| 				el("input", { | ||||
| 					type: "text", | ||||
| 					placeholder: "Search tasks...", | ||||
| 					value: searchQuery.get() | ||||
| 				}, on("input", e => searchQuery.set(e.target.value))), | ||||
| 				el("select", null, | ||||
| 					on.host(el=> el.value= filterPriority.get()), | ||||
| 					on("change", e => filterPriority.set(e.target.value)) | ||||
| 				).append( | ||||
| 					el("option", { value: "all", textContent: "All Priorities" }), | ||||
| 					el("option", { value: "low", textContent: "Low Priority" }), | ||||
| 					el("option", { value: "medium", textContent: "Medium Priority" }), | ||||
| 					el("option", { value: "high", textContent: "High Priority" }) | ||||
| 				) | ||||
| 			) | ||||
| 		), | ||||
|  | ||||
| 		// Add new task form | ||||
| 		el("form", { className: "new-task-form" }, on("submit", onAddTask)).append( | ||||
| 			el("div", { className: "form-row" }).append( | ||||
| 				el("input", { | ||||
| 					type: "text", | ||||
| 					placeholder: "New task title", | ||||
| 					value: newTaskTitle.get(), | ||||
| 					required: true | ||||
| 				}, on("input", e => newTaskTitle.set(e.target.value))), | ||||
| 				el("select", null, | ||||
| 					on.host(el=> el.value= newTaskPriority.get()), | ||||
| 					on("change", e => newTaskPriority.set(e.target.value)) | ||||
| 				).append( | ||||
| 					el("option", { value: "low", textContent: "Low" }), | ||||
| 					el("option", { value: "medium", textContent: "Medium" }), | ||||
| 					el("option", { value: "high", textContent: "High" }) | ||||
| 				), | ||||
| 				el("button", { type: "submit", className: "add-btn" }).append("Add Task") | ||||
| 			), | ||||
| 			el("textarea", { | ||||
| 				placeholder: "Task description (optional)", | ||||
| 				value: newTaskDescription.get() | ||||
| 			}, on("input", e => newTaskDescription.set(e.target.value))) | ||||
| 		), | ||||
|  | ||||
| 		// Task board with columns | ||||
| 		el("div", { className: "task-board" }).append( | ||||
| 			// Todo column | ||||
| 			el("div", { | ||||
| 				id: `column-${STATUSES.TODO}`, | ||||
| 				className: "task-column" | ||||
| 			}, onDragArea(STATUSES.TODO)).append( | ||||
| 				el("h2", { className: "column-header" }).append( | ||||
| 					"To Do ", | ||||
| 					el("span", { className: "task-count" }).append( | ||||
| 						S(() => tasksByStatus.get()[STATUSES.TODO].length) | ||||
| 					) | ||||
| 				), | ||||
| 				S.el(S(() => tasksByStatus.get()[STATUSES.TODO]), tasks => | ||||
| 					el("div", { className: "column-tasks" }).append( | ||||
| 						...tasks.map(renderTaskCard) | ||||
| 					) | ||||
| 				) | ||||
| 			), | ||||
|  | ||||
| 			// In Progress column | ||||
| 			el("div", { | ||||
| 				id: `column-${STATUSES.IN_PROGRESS}`, | ||||
| 				className: "task-column" | ||||
| 			}, onDragArea(STATUSES.IN_PROGRESS)).append( | ||||
| 				el("h2", { className: "column-header" }).append( | ||||
| 					"In Progress ", | ||||
| 					el("span", { className: "task-count" }).append( | ||||
| 						S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS].length) | ||||
| 					) | ||||
| 				), | ||||
| 				S.el(S(() => tasksByStatus.get()[STATUSES.IN_PROGRESS]), tasks => | ||||
| 					el("div", { className: "column-tasks" }).append( | ||||
| 						...tasks.map(renderTaskCard) | ||||
| 					) | ||||
| 				) | ||||
| 			), | ||||
|  | ||||
| 			// Done column | ||||
| 			el("div", { | ||||
| 				id: `column-${STATUSES.DONE}`, | ||||
| 				className: "task-column" | ||||
| 			}, onDragArea(STATUSES.DONE)).append( | ||||
| 				el("h2", { className: "column-header" }).append( | ||||
| 					"Done ", | ||||
| 					el("span", { className: "task-count" }).append( | ||||
| 						S(() => tasksByStatus.get()[STATUSES.DONE].length) | ||||
| 					) | ||||
| 				), | ||||
| 				S.el(S(() => tasksByStatus.get()[STATUSES.DONE]), tasks => | ||||
| 					el("div", { className: "column-tasks" }).append( | ||||
| 						...tasks.map(renderTaskCard) | ||||
| 					) | ||||
| 				) | ||||
| 			) | ||||
| 		), | ||||
| 	); | ||||
| } | ||||
|  | ||||
| // Render the component | ||||
| document.body.append( | ||||
| 	el("div", { style: "padding: 20px; background: #f5f5f5; min-height: 100vh;" }).append( | ||||
| 		el(TaskManager) | ||||
| 	), | ||||
| 	el("style", ` | ||||
| 		.task-manager { | ||||
| 			font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||||
| 			max-width: 1200px; | ||||
| 			margin: 0 auto; | ||||
| 			padding: 1rem; | ||||
| 			color: #333; | ||||
| 		} | ||||
|  | ||||
| 		.app-header { | ||||
| 			display: flex; | ||||
| 			justify-content: space-between; | ||||
| 			align-items: center; | ||||
| 			margin-bottom: 1.5rem; | ||||
| 			flex-wrap: wrap; | ||||
| 			gap: 1rem; | ||||
| 		} | ||||
|  | ||||
| 		.app-header h1 { | ||||
| 			margin: 0; | ||||
| 			color: #2d3748; | ||||
| 		} | ||||
|  | ||||
| 		.app-controls { | ||||
| 			display: flex; | ||||
| 			gap: 1rem; | ||||
| 		} | ||||
|  | ||||
| 		.app-controls input, | ||||
| 		.app-controls select { | ||||
| 			padding: 0.5rem; | ||||
| 			border: 1px solid #e2e8f0; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 0.9rem; | ||||
| 		} | ||||
|  | ||||
| 		.new-task-form { | ||||
| 			background: white; | ||||
| 			padding: 1.5rem; | ||||
| 			border-radius: 8px; | ||||
| 			box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | ||||
| 			margin-bottom: 2rem; | ||||
| 		} | ||||
|  | ||||
| 		.form-row { | ||||
| 			display: flex; | ||||
| 			gap: 1rem; | ||||
| 			margin-bottom: 1rem; | ||||
| 		} | ||||
|  | ||||
| 		.form-row input { | ||||
| 			flex-grow: 1; | ||||
| 			padding: 0.75rem; | ||||
| 			border: 1px solid #e2e8f0; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 1rem; | ||||
| 		} | ||||
|  | ||||
| 		.form-row select { | ||||
| 			width: 100px; | ||||
| 			padding: 0.75rem; | ||||
| 			border: 1px solid #e2e8f0; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 1rem; | ||||
| 		} | ||||
|  | ||||
| 		.add-btn { | ||||
| 			background: #4a90e2; | ||||
| 			color: white; | ||||
| 			border: none; | ||||
| 			border-radius: 4px; | ||||
| 			padding: 0.75rem 1.5rem; | ||||
| 			font-size: 1rem; | ||||
| 			cursor: pointer; | ||||
| 			transition: background 0.2s ease; | ||||
| 		} | ||||
|  | ||||
| 		.add-btn:hover { | ||||
| 			background: #3a7bc8; | ||||
| 		} | ||||
|  | ||||
| 		.new-task-form textarea { | ||||
| 			width: 100%; | ||||
| 			padding: 0.75rem; | ||||
| 			border: 1px solid #e2e8f0; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 1rem; | ||||
| 			resize: vertical; | ||||
| 			min-height: 80px; | ||||
| 		} | ||||
|  | ||||
| 		.task-board { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | ||||
| 			gap: 1.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.task-column { | ||||
| 			background: #f7fafc; | ||||
| 			border-radius: 8px; | ||||
| 			padding: 1rem; | ||||
| 			min-height: 400px; | ||||
| 			transition: background 0.2s ease; | ||||
| 		} | ||||
|  | ||||
| 		.column-header { | ||||
| 			margin-top: 0; | ||||
| 			padding-bottom: 0.75rem; | ||||
| 			border-bottom: 2px solid #e2e8f0; | ||||
| 			font-size: 1.25rem; | ||||
| 			color: #2d3748; | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 		} | ||||
|  | ||||
| 		.task-count { | ||||
| 			display: inline-flex; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 			background: #e2e8f0; | ||||
| 			color: #4a5568; | ||||
| 			border-radius: 50%; | ||||
| 			width: 25px; | ||||
| 			height: 25px; | ||||
| 			font-size: 0.875rem; | ||||
| 			margin-left: 0.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.column-tasks { | ||||
| 			margin-top: 1rem; | ||||
| 			display: flex; | ||||
| 			flex-direction: column; | ||||
| 			gap: 1rem; | ||||
| 			min-height: 200px; | ||||
| 		} | ||||
|  | ||||
| 		.task-card { | ||||
| 			background: white; | ||||
| 			border-radius: 6px; | ||||
| 			padding: 1rem; | ||||
| 			box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | ||||
| 			cursor: grab; | ||||
| 			transition: transform 0.2s ease, box-shadow 0.2s ease; | ||||
| 			position: relative; | ||||
| 			border-left: 4px solid #ccc; | ||||
| 		} | ||||
|  | ||||
| 		.task-card:hover { | ||||
| 			transform: translateY(-3px); | ||||
| 			box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1); | ||||
| 		} | ||||
|  | ||||
| 		.task-card.dragging { | ||||
| 			opacity: 0.5; | ||||
| 			cursor: grabbing; | ||||
| 		} | ||||
|  | ||||
| 		.task-card.priority-low { | ||||
| 			border-left-color: #38b2ac; | ||||
| 		} | ||||
|  | ||||
| 		.task-card.priority-medium { | ||||
| 			border-left-color: #ecc94b; | ||||
| 		} | ||||
|  | ||||
| 		.task-card.priority-high { | ||||
| 			border-left-color: #e53e3e; | ||||
| 		} | ||||
|  | ||||
| 		.task-header { | ||||
| 			display: flex; | ||||
| 			justify-content: space-between; | ||||
| 			align-items: flex-start; | ||||
| 			margin-bottom: 0.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.task-title { | ||||
| 			margin: 0; | ||||
| 			font-size: 1.1rem; | ||||
| 			color: #2d3748; | ||||
| 			word-break: break-word; | ||||
| 		} | ||||
|  | ||||
| 		.task-description { | ||||
| 			margin: 0.5rem 0; | ||||
| 			font-size: 0.9rem; | ||||
| 			color: #4a5568; | ||||
| 			word-break: break-word; | ||||
| 		} | ||||
|  | ||||
| 		.task-actions { | ||||
| 			display: flex; | ||||
| 			gap: 0.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.edit-btn, | ||||
| 		.delete-btn { | ||||
| 			background: none; | ||||
| 			border: none; | ||||
| 			font-size: 1rem; | ||||
| 			cursor: pointer; | ||||
| 			width: 24px; | ||||
| 			height: 24px; | ||||
| 			display: inline-flex; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 			border-radius: 50%; | ||||
| 			color: #718096; | ||||
| 			transition: background 0.2s ease, color 0.2s ease; | ||||
| 		} | ||||
|  | ||||
| 		.edit-btn:hover { | ||||
| 			background: #edf2f7; | ||||
| 			color: #4a5568; | ||||
| 		} | ||||
|  | ||||
| 		.delete-btn:hover { | ||||
| 			background: #fed7d7; | ||||
| 			color: #e53e3e; | ||||
| 		} | ||||
|  | ||||
| 		.task-meta { | ||||
| 			display: flex; | ||||
| 			justify-content: space-between; | ||||
| 			align-items: center; | ||||
| 			margin-top: 0.75rem; | ||||
| 		} | ||||
|  | ||||
| 		.priority-badge { | ||||
| 			font-size: 0.75rem; | ||||
| 			padding: 0.2rem 0.5rem; | ||||
| 			border-radius: 4px; | ||||
| 			font-weight: 500; | ||||
| 		} | ||||
|  | ||||
| 		.priority-badge.priority-low { | ||||
| 			background: #e6fffa; | ||||
| 			color: #2c7a7b; | ||||
| 		} | ||||
|  | ||||
| 		.priority-badge.priority-medium { | ||||
| 			background: #fefcbf; | ||||
| 			color: #975a16; | ||||
| 		} | ||||
|  | ||||
| 		.priority-badge.priority-high { | ||||
| 			background: #fed7d7; | ||||
| 			color: #c53030; | ||||
| 		} | ||||
|  | ||||
| 		.drag-over { | ||||
| 			background: #f0f9ff; | ||||
| 			border: 2px dashed #4a90e2; | ||||
| 		} | ||||
|  | ||||
| 		.task-edit-form { | ||||
| 			display: flex; | ||||
| 			flex-direction: column; | ||||
| 			gap: 0.75rem; | ||||
| 		} | ||||
|  | ||||
| 		.task-title-input, | ||||
| 		.task-desc-input { | ||||
| 			width: 100%; | ||||
| 			padding: 0.5rem; | ||||
| 			border: 1px solid #e2e8f0; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 0.9rem; | ||||
| 		} | ||||
|  | ||||
| 		.task-desc-input { | ||||
| 			min-height: 60px; | ||||
| 			resize: vertical; | ||||
| 		} | ||||
|  | ||||
| 		.task-edit-actions { | ||||
| 			display: flex; | ||||
| 			justify-content: flex-end; | ||||
| 			gap: 0.5rem; | ||||
| 			margin-top: 0.5rem; | ||||
| 		} | ||||
|  | ||||
| 		.cancel-btn, | ||||
| 		.save-btn { | ||||
| 			padding: 0.4rem 0.75rem; | ||||
| 			border-radius: 4px; | ||||
| 			font-size: 0.9rem; | ||||
| 			cursor: pointer; | ||||
| 		} | ||||
|  | ||||
| 		.cancel-btn { | ||||
| 			background: #edf2f7; | ||||
| 			color: #4a5568; | ||||
| 			border: 1px solid #e2e8f0; | ||||
| 		} | ||||
|  | ||||
| 		.save-btn { | ||||
| 			background: #4a90e2; | ||||
| 			color: white; | ||||
| 			border: none; | ||||
| 		} | ||||
|  | ||||
| 		@media (max-width: 768px) { | ||||
| 			.app-header { | ||||
| 				flex-direction: column; | ||||
| 				align-items: flex-start; | ||||
| 			} | ||||
|  | ||||
| 			.app-controls { | ||||
| 				width: 100%; | ||||
| 				flex-direction: column; | ||||
| 			} | ||||
|  | ||||
| 			.form-row { | ||||
| 				flex-direction: column; | ||||
| 			} | ||||
|  | ||||
| 			.task-board { | ||||
| 				grid-template-columns: 1fr; | ||||
| 			} | ||||
| 		} | ||||
| 	`) | ||||
| ); | ||||
							
								
								
									
										63
									
								
								docs/p15-examples.html.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								docs/p15-examples.html.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import { T, t } from "./utils/index.js"; | ||||
| export const info= { | ||||
| 	title: t`Examples Gallery`, | ||||
| 	fullTitle: t`DDE Examples & Code Snippets`, | ||||
| 	description: t`A comprehensive collection of examples and code snippets for working with Deka DOM Elements.`, | ||||
| }; | ||||
|  | ||||
| import { el } from "deka-dom-el"; | ||||
| import { simplePage } from "./layout/simplePage.html.js"; | ||||
| import { h3 } from "./components/pageUtils.html.js"; | ||||
| import { example } from "./components/example.html.js"; | ||||
| /** @param {string} url */ | ||||
| const fileURL= url=> new URL(url, import.meta.url); | ||||
|  | ||||
| /** @param {import("./types.d.ts").PageAttrs} attrs */ | ||||
| export function page({ pkg, info }){ | ||||
| 	const page_id= info.id; | ||||
| 	return el(simplePage, { info, pkg }).append( | ||||
| 		el("p").append(T` | ||||
| 			Real-world application examples showcasing how to build complete, production-ready interfaces with dd<el>: | ||||
| 		`), | ||||
| 		el(h3, t`Data Dashboard`), | ||||
| 		el("p").append(T` | ||||
| 			Data visualization dashboard with charts, filters, and responsive layout.  Integration with a | ||||
| 			third-party charting library, data fetching and state management, responsive layout design, and multiple | ||||
| 			interactive components working together. | ||||
| 		`), | ||||
| 		el(example, { src: fileURL("./components/examples/case-studies/data-dashboard.js"), variant: "big", page_id }), | ||||
|  | ||||
| 		el(h3, t`Interactive Form`), | ||||
| 		el("p").append(T` | ||||
| 			Complete form with real-time validation, conditional rendering, and responsive design. Form handling with | ||||
| 			real-time validation, reactive UI updates, complex form state management, and clean separation of concerns. | ||||
| 		`), | ||||
| 		el(example, { src: fileURL("./components/examples/case-studies/interactive-form.js"), variant: "big", page_id }), | ||||
|  | ||||
|  | ||||
| 		el(h3, t`Interactive Image Gallery`), | ||||
| 		el("p").append(T` | ||||
| 			Responsive image gallery with lightbox, keyboard navigation, and filtering. Dynamic loading of content, | ||||
| 			lightbox functionality, animation handling, and keyboard and gesture navigation support. | ||||
| 		`), | ||||
| 		el(example, { src: fileURL("./components/examples/case-studies/image-gallery.js"), variant: "big", page_id }), | ||||
|  | ||||
|  | ||||
| 		el(h3, t`Task Manager`), | ||||
| 		el("p").append(T` | ||||
| 			Kanban-style task management app with drag-and-drop and localStorage persistence. Complex state management | ||||
| 			with signals, drag and drop functionality, local storage persistence, and responsive design for different | ||||
| 			devices. | ||||
| 		`), | ||||
| 		el(example, { src: fileURL("./components/examples/case-studies/task-manager.js"), variant: "big", page_id }), | ||||
|  | ||||
|  | ||||
| 		el(h3, t`TodoMVC`), | ||||
| 		el("p").append(T` | ||||
| 			Complete TodoMVC implementation with local storage and routing. TodoMVC implementation showing routing, | ||||
| 			local storage persistence, filtering, and component architecture patterns. For commented code, see the | ||||
| 			dedicated page ${el("a", { href: "./p10-todomvc.html" }).append(T`TodoMVC`)}. | ||||
| 		`), | ||||
|  | ||||
| 	); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user