diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3eda0ea --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "env": { + "browser": true, + "es2021": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "indent": ["error", 4], + "linebreak-style": ["error", "windows"], + "quotes": ["error", "single"], + "semi": ["error", "always"], + "no-unused-vars": ["warn"], + "no-console": ["warn", { "allow": ["warn", "error"] }], + "prefer-const": "error", + "arrow-body-style": ["error", "as-needed"], + "curly": "error", + "eqeqeq": "error", + "no-var": "error" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4d4167 --- /dev/null +++ b/.gitignore @@ -0,0 +1,99 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock + +# Build output +dist/ +build/ +*.min.js +*.min.css + +# IDE and editor files +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# Environment variables +.env +.env.local +.env.*.local + +# Testing +coverage/ +.nyc_output/ +jest-results/ + +# Temporary files +*.log +*.tmp +*.temp +.cache/ + +# Generated files +pages/* +!pages/.gitkeep + +# Image source files and working copies +*.psd +*.ai +*.sketch +*.fig +*.xd +*.xcf +*.raw +*.cr2 +*.nef +*.dng + +# Image optimization caches +.imagemin-cache/ +.responsive-images-cache/ + +# Generated image formats +images/**/*.webp +images/**/*-resized.* +images/**/*-thumbnail.* +images/**/*-optimized.* + +# Keep original images +!images/**/*.svg +!images/**/*.png +!images/**/*.jpg +!images/**/*.jpeg +!images/**/*.gif + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Debug logs +debug.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Rollup build cache +.rollup.cache/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4064a2d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2024-12-24 + +### Added +- Initial release of Digital Services Hub +- Text to Speech tool with multiple voice options +- Image Resizer with aspect ratio preservation +- Color Palette generator with harmony options +- ASCII Art converter with customization +- QR Code generator with styling options +- Base Tool class for shared functionality +- Utility modules for common operations +- Theme support (light/dark) +- Keyboard shortcuts +- Responsive design +- Accessibility features + +### Security +- Input sanitization +- XSS prevention +- CSRF protection +- Content Security Policy + +### Documentation +- README.md with feature documentation +- PROJECT-DESCRIPTION.md with technical details +- PROJECT-OVERVIEW.md with architecture details +- FUTURE-FEATURES.md with roadmap +- Code documentation with JSDoc +- Contributing guidelines \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cd581d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to Digital Services Hub + +We love your input! We want to make contributing to Digital Services Hub as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + +## Development Process + +We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. + +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! + +## Pull Request Process + +1. Update the README.md with details of changes to the interface, if applicable. +2. Update the CHANGELOG.md with a note describing your changes. +3. The PR will be merged once you have the sign-off of at least one maintainer. + +## Any contributions you make will be under the MIT Software License + +In short, when you submit code changes, your submissions are understood to be under the same [MIT License](LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using GitHub's [issue tracker] + +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](). + +## Write bug reports with detail, background, and sample code + +**Great Bug Reports** tend to have: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! + - Give sample code if you can. +- What you expected would happen +- What actually happens +- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) + +## Code Style + +* Use 4 spaces for indentation +* Use camelCase for variable and function names +* Add JSDoc comments for functions +* Follow ESLint rules + +## License + +By contributing, you agree that your contributions will be licensed under its MIT License. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7d22a6d..0665dd2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 T +Copyright (c) 2024 Digital Services Hub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md deleted file mode 100644 index 482b67a..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# Digital_Services.HUB \ No newline at end of file diff --git a/css/base/layout.css b/css/base/layout.css new file mode 100644 index 0000000..fa8d865 --- /dev/null +++ b/css/base/layout.css @@ -0,0 +1,72 @@ +/* Container */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Grid layouts */ +.grid { + display: grid; + gap: 2rem; +} + +.tools-grid { + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); +} + +.features-grid { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +/* Flexbox layouts */ +.flex { + display: flex; +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +/* Spacing utilities */ +.mt-1 { margin-top: 0.5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } +.mt-4 { margin-top: 2rem; } + +.mb-1 { margin-bottom: 0.5rem; } +.mb-2 { margin-bottom: 1rem; } +.mb-3 { margin-bottom: 1.5rem; } +.mb-4 { margin-bottom: 2rem; } + +.mx-auto { margin-left: auto; margin-right: auto; } + +/* Responsive utilities */ +@media (max-width: 768px) { + .container { + padding: 0 0.5rem; + } + + .tools-grid, + .features-grid { + grid-template-columns: 1fr; + } + + .flex-responsive { + flex-direction: column; + } +} \ No newline at end of file diff --git a/css/base/reset.css b/css/base/reset.css new file mode 100644 index 0000000..0bb5af6 --- /dev/null +++ b/css/base/reset.css @@ -0,0 +1,66 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Typography */ +body { + font-family: 'Roboto', sans-serif; + line-height: 1.6; +} + +h1, h2, h3, h4 { + font-family: 'Orbitron', sans-serif; + margin-bottom: 1rem; +} + +a { + text-decoration: none; + color: inherit; + transition: var(--transition); +} + +a:hover { + color: var(--primary-color); +} + +/* Lists */ +ul, ol { + list-style: none; +} + +/* Images */ +img { + max-width: 100%; + height: auto; + display: block; +} + +/* Form elements */ +button, +input, +select, +textarea { + font: inherit; + color: inherit; + border: none; + background: none; +} + +button { + cursor: pointer; +} + +/* Focus styles */ +:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Selection */ +::selection { + background-color: var(--primary-color); + color: var(--text-light); +} \ No newline at end of file diff --git a/css/color-palette.css b/css/color-palette.css deleted file mode 100644 index 8ba5011..0000000 --- a/css/color-palette.css +++ /dev/null @@ -1,17 +0,0 @@ -#palette-container { - display: flex; - justify-content: space-between; - margin-top: 1rem; -} - -.color-swatch { - width: 50px; - height: 50px; - border-radius: 50%; -} - -.color-info { - text-align: center; - font-size: 0.8rem; - margin-top: 0.5rem; -} diff --git a/css/components/alerts.css b/css/components/alerts.css new file mode 100644 index 0000000..adc2432 --- /dev/null +++ b/css/components/alerts.css @@ -0,0 +1,207 @@ +/** + * Alert Component Styles + */ + +.alert-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 400px; +} + +.alert { + position: relative; + display: flex; + align-items: flex-start; + gap: var(--spacing-3); + padding: var(--spacing-3); + margin-bottom: var(--spacing-4); + border-radius: var(--radius-lg); + border-left: 4px solid transparent; +} + +.alert.alert-closing { + animation: slideOut 0.3s ease-out forwards; +} + +.alert-content { + flex: 1; +} + +.alert-icon { + flex-shrink: 0; + width: 20px; + height: 20px; +} + +.alert-title { + margin: 0 0 var(--spacing-1); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); +} + +.alert-message { + margin: 0; + font-size: var(--font-size-sm); + line-height: var(--line-height-normal); +} + +.alert-close { + position: absolute; + top: var(--spacing-3); + right: var(--spacing-3); + padding: var(--spacing-1); + color: currentColor; + background: transparent; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + opacity: 0.6; + transition: var(--transition-opacity); +} + +.alert-close:hover { + opacity: 1; +} + +.alert-close:focus { + outline: none; + box-shadow: 0 0 0 2px var(--primary-alpha); +} + +/* Alert Variants */ +.alert-info { + color: var(--info-color); + background: var(--info-bg); + border-left-color: var(--info-color); +} + +.alert-success { + color: var(--success-color); + background: var(--success-bg); + border-left-color: var(--success-color); +} + +.alert-warning { + color: var(--warning-color); + background: var(--warning-bg); + border-left-color: var(--warning-color); +} + +.alert-error { + color: var(--error-color); + background: var(--error-bg); + border-left-color: var(--error-color); +} + +/* Alert Sizes */ +.alert-sm { + padding: var(--spacing-2); + font-size: var(--font-size-sm); +} + +.alert-lg { + padding: var(--spacing-4); + font-size: var(--font-size-lg); +} + +/* Alert with Actions */ +.alert-actions { + display: flex; + gap: var(--spacing-2); + margin-top: var(--spacing-2); +} + +.alert-action { + padding: var(--spacing-1) var(--spacing-2); + color: currentColor; + font-size: var(--font-size-sm); + background: transparent; + border: 1px solid currentColor; + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition-colors); +} + +.alert-action:hover { + background: rgba(currentColor, 0.1); +} + +.alert-action:focus { + outline: none; + box-shadow: 0 0 0 2px var(--primary-alpha); +} + +/* Alert Animation */ +.alert-enter { + opacity: 0; + transform: translateY(-1rem); +} + +.alert-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity 200ms ease-out, transform 200ms ease-out; +} + +.alert-exit { + opacity: 1; + transform: translateY(0); +} + +.alert-exit-active { + opacity: 0; + transform: translateY(-1rem); + transition: opacity 150ms ease-in, transform 150ms ease-in; +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .alert-info { + background: var(--info-bg-dark); +} + +[data-theme="dark"] .alert-success { + background: var(--success-bg-dark); +} + +[data-theme="dark"] .alert-warning { + background: var(--warning-bg-dark); +} + +[data-theme="dark"] .alert-error { + background: var(--error-bg-dark); +} + +/* Accessibility */ +.alert { + role: "alert"; + aria-live: "polite"; +} + +/* Animations */ +@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; + } +} diff --git a/css/components/ascii-art.css b/css/components/ascii-art.css new file mode 100644 index 0000000..1a29d06 --- /dev/null +++ b/css/components/ascii-art.css @@ -0,0 +1,246 @@ +.ascii-controls { + background: var(--card-bg); + border-radius: 10px; + padding: 2rem; + margin-top: 2rem; +} + +/* Drop Zone */ +.drop-zone { + border: 2px dashed var(--primary-color); + border-radius: 10px; + padding: 2rem; + text-align: center; + cursor: pointer; + transition: var(--transition); + margin-bottom: 2rem; + position: relative; +} + +.drop-zone:hover, +.drop-zone.dragover { + background: rgba(var(--primary-color-rgb), 0.1); + border-color: var(--secondary-color); +} + +.drop-zone-text { + color: var(--text-light); +} + +.drop-zone-text .small { + font-size: 0.9rem; + opacity: 0.7; +} + +.file-input { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + cursor: pointer; +} + +/* Preview Container */ +.preview-container { + margin: 2rem 0; + text-align: center; + display: none; +} + +.preview-container.active { + display: block; +} + +.preview-container img { + max-width: 100%; + max-height: 300px; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); +} + +/* Settings Grid */ +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin: 2rem 0; +} + +.input-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.input-group label { + color: var(--text-light); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.input-group input[type="number"], +.input-group select { + padding: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + background: rgba(255, 255, 255, 0.05); + color: var(--text-light); + width: 100%; + transition: var(--transition); +} + +.input-group input[type="number"]:focus, +.input-group select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1); +} + +.input-group input[type="checkbox"] { + width: 1.2rem; + height: 1.2rem; + border: 1px solid var(--primary-color); + border-radius: 3px; + background: rgba(255, 255, 255, 0.05); + cursor: pointer; + position: relative; + appearance: none; + -webkit-appearance: none; + transition: var(--transition); +} + +.input-group input[type="checkbox"]:checked { + background: var(--primary-color); +} + +.input-group input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--text-dark); + font-size: 0.8rem; +} + +/* Output Container */ +.output-container { + margin-top: 2rem; + display: none; +} + +.output-container.active { + display: block; + animation: fadeIn 0.3s ease-out; +} + +.ascii-output { + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + padding: 1.5rem; + overflow-x: auto; + white-space: pre; + font-family: 'Courier New', monospace; + font-size: 12px; + line-height: 1; + color: var(--text-light); + margin-bottom: 1rem; + max-height: 500px; + overflow-y: auto; +} + +.output-actions { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 1rem; +} + +/* Buttons */ +.tech-button { + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: var(--text-dark); + border: none; + padding: 0.75rem 1.5rem; + border-radius: 5px; + font-weight: bold; + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; +} + +.tech-button:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); +} + +.tech-button.small { + padding: 0.5rem 1rem; + font-size: 0.9rem; + width: auto; +} + +.tech-button i { + font-size: 1.1rem; +} + +/* Loading State */ +.loading { + position: relative; + pointer-events: none; + opacity: 0.7; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 1.5rem; + height: 1.5rem; + border: 2px solid transparent; + border-top-color: var(--text-dark); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .settings-grid { + grid-template-columns: 1fr; + } + + .output-actions { + flex-direction: column; + } + + .tech-button.small { + width: 100%; + } +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/css/components/buttons.css b/css/components/buttons.css new file mode 100644 index 0000000..c82f8e2 --- /dev/null +++ b/css/components/buttons.css @@ -0,0 +1,230 @@ +/* Base Button */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-4); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); + text-align: center; + text-decoration: none; + white-space: nowrap; + border: 1px solid transparent; + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition-colors), var(--transition-shadow); +} + +/* Button Variants */ +.btn-primary { + color: var(--white); + background: var(--gradient-primary); + border-color: transparent; +} + +.btn-primary:hover { + background: var(--gradient-primary-hover); +} + +.btn-primary:focus { + outline: none; + box-shadow: 0 0 0 2px var(--primary-alpha); +} + +.btn-secondary { + color: var(--text-primary); + background: var(--bg-secondary); + border-color: var(--border-color); +} + +.btn-secondary:hover { + background: var(--bg-hover); + border-color: var(--border-hover); +} + +.btn-secondary:focus { + outline: none; + box-shadow: 0 0 0 2px var(--primary-alpha); +} + +.btn-success { + color: var(--white); + background: var(--gradient-success); + border-color: transparent; +} + +.btn-success:hover { + background: var(--gradient-success-hover); +} + +.btn-success:focus { + outline: none; + box-shadow: 0 0 0 2px var(--success-alpha); +} + +.btn-warning { + color: var(--white); + background: var(--gradient-warning); + border-color: transparent; +} + +.btn-warning:hover { + background: var(--gradient-warning-hover); +} + +.btn-warning:focus { + outline: none; + box-shadow: 0 0 0 2px var(--warning-alpha); +} + +.btn-error { + color: var(--white); + background: var(--gradient-error); + border-color: transparent; +} + +.btn-error:hover { + background: var(--gradient-error-hover); +} + +.btn-error:focus { + outline: none; + box-shadow: 0 0 0 2px var(--error-alpha); +} + +/* Button Sizes */ +.btn-sm { + padding: var(--spacing-1) var(--spacing-2); + font-size: var(--font-size-xs); +} + +.btn-lg { + padding: var(--spacing-3) var(--spacing-6); + font-size: var(--font-size-base); +} + +/* Button States */ +.btn:disabled, +.btn.disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +.btn.loading { + position: relative; + color: transparent; +} + +.btn.loading::after { + content: ""; + position: absolute; + width: 1em; + height: 1em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: var(--radius-full); + animation: button-loading 0.75s infinite linear; +} + +@keyframes button-loading { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Button with Icon */ +.btn-icon { + padding: var(--spacing-2); + border-radius: var(--radius-md); +} + +.btn-icon.btn-sm { + padding: var(--spacing-1); +} + +.btn-icon.btn-lg { + padding: var(--spacing-3); +} + +/* Button Group */ +.btn-group { + display: inline-flex; + border-radius: var(--radius-md); +} + +.btn-group .btn { + border-radius: 0; +} + +.btn-group .btn:first-child { + border-top-left-radius: var(--radius-md); + border-bottom-left-radius: var(--radius-md); +} + +.btn-group .btn:last-child { + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); +} + +.btn-group .btn:not(:first-child) { + margin-left: -1px; +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .btn-secondary { + background: var(--bg-tertiary); +} + +[data-theme="dark"] .btn-secondary:hover { + background: var(--bg-hover-dark); +} + +/* Accessibility */ +.btn { + position: relative; +} + +.btn:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +.btn[aria-pressed="true"] { + background: var(--bg-active); +} + +.btn[aria-expanded="true"] { + background: var(--bg-active); +} + +/* Button Link */ +.btn-link { + padding: 0; + color: var(--primary-color); + background: transparent; + border: none; + text-decoration: underline; +} + +.btn-link:hover { + color: var(--primary-hover); + text-decoration: none; +} + +.btn-link:focus { + outline: none; + box-shadow: none; + text-decoration: none; +} + +/* Button Block */ +.btn-block { + display: flex; + width: 100%; +} diff --git a/css/components/cards.css b/css/components/cards.css new file mode 100644 index 0000000..d95174b --- /dev/null +++ b/css/components/cards.css @@ -0,0 +1,191 @@ +/** + * Card Components + */ + +/* Base card */ +.card { + position: relative; + display: flex; + flex-direction: column; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; + transition: var(--transition-all); +} + +.card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +/* Card Header */ +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-4); + border-bottom: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +.card-title { + margin: 0; + color: var(--text-primary); + font-size: var(--font-size-lg); + font-weight: 600; + line-height: var(--line-height-tight); +} + +/* Card Body */ +.card-body { + flex: 1; + padding: var(--spacing-4); +} + +.card-text { + color: var(--text-secondary); + font-size: var(--font-size-sm); + line-height: var(--line-height-normal); +} + +/* Card Footer */ +.card-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-2); + padding: var(--spacing-4); + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +/* Card Image */ +.card-image { + position: relative; + padding-top: 56.25%; /* 16:9 Aspect Ratio */ + background: var(--bg-secondary); + overflow: hidden; +} + +.card-image img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: var(--transition-transform); +} + +.card:hover .card-image img { + transform: scale(1.05); +} + +/* Card Badge */ +.card-badge { + position: absolute; + top: var(--spacing-3); + right: var(--spacing-3); + padding: var(--spacing-1) var(--spacing-2); + color: var(--text-light); + font-size: var(--font-size-xs); + font-weight: 500; + line-height: var(--line-height-tight); + background: var(--primary-color); + border-radius: var(--radius-full); +} + +/* Card Overlay */ +.card-overlay { + position: relative; + overflow: hidden; +} + +.card-overlay::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent, rgba(var(--dark-bg-primary), 0.8)); + opacity: 0; + transition: var(--transition-opacity); +} + +.card-overlay:hover::before { + opacity: 1; +} + +.card-overlay .card-content { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: var(--spacing-4); + transform: translateY(100%); + transition: var(--transition-transform); +} + +.card-overlay:hover .card-content { + transform: translateY(0); +} + +/* Card Horizontal */ +.card-horizontal { + flex-direction: row; +} + +.card-horizontal .card-image { + flex: 0 0 40%; + padding-top: 0; +} + +.card-horizontal .card-content { + flex: 1; + display: flex; + flex-direction: column; +} + +@media (max-width: 640px) { + .card-horizontal { + flex-direction: column; + } + + .card-horizontal .card-image { + padding-top: 56.25%; + } +} + +/* Card Grid */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--spacing-4); +} + +/* Card List */ +.card-list { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .card { + background: var(--bg-secondary); +} + +[data-theme="dark"] .card-header, +[data-theme="dark"] .card-footer { + background: var(--bg-tertiary); +} + +[data-theme="dark"] .card-image { + background: var(--bg-tertiary); +} + +[data-theme="dark"] .card-overlay::before { + background: linear-gradient(to bottom, transparent, rgba(var(--dark-bg-primary), 0.9)); +} diff --git a/css/components/color-palette.css b/css/components/color-palette.css new file mode 100644 index 0000000..5bf2224 --- /dev/null +++ b/css/components/color-palette.css @@ -0,0 +1,350 @@ +.palette-controls { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Color Picker Section */ +.color-picker-section { + background: var(--bg-secondary); + border-radius: 8px; + padding: 2rem; + margin-bottom: 2rem; +} + +.color-picker-container { + display: grid; + grid-template-columns: auto 1fr; + gap: 2rem; + align-items: center; + margin-bottom: 2rem; +} + +.color-wheel { + width: 250px; + height: 250px; +} + +.color-sliders { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Harmony Controls */ +.harmony-controls { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 1rem; + align-items: center; +} + +/* Generated Palette */ +.generated-palette { + background: var(--bg-secondary); + border-radius: 8px; + padding: 2rem; + margin-bottom: 2rem; +} + +.palette-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.palette-actions { + display: flex; + gap: 1rem; +} + +.color-swatches { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +.color-swatch { + aspect-ratio: 1; + border-radius: 8px; + cursor: pointer; + transition: transform 0.3s ease; + position: relative; + overflow: hidden; +} + +.color-swatch:hover { + transform: scale(1.05); +} + +/* Export Dropdown */ +.export-dropdown { + position: relative; +} + +.export-menu { + position: absolute; + top: 100%; + right: 0; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.5rem; + display: none; + z-index: 100; +} + +.export-menu.show { + display: block; +} + +.export-menu button { + display: block; + width: 100%; + padding: 0.5rem 1rem; + text-align: left; + background: none; + border: none; + color: var(--text-primary); + cursor: pointer; + white-space: nowrap; +} + +.export-menu button:hover { + background: var(--bg-hover); +} + +/* Saved Palettes */ +.saved-palettes { + background: var(--bg-secondary); + border-radius: 8px; + padding: 2rem; +} + +.saved-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.saved-palette-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1.5rem; +} + +.saved-palette { + background: var(--bg-primary); + border-radius: 8px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.saved-swatches { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.5rem; +} + +.saved-swatch { + aspect-ratio: 1; + border-radius: 4px; +} + +.saved-palette-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--bg-primary); + padding: 2rem; + border-radius: 8px; + min-width: 300px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.close-modal { + background: none; + border: none; + color: var(--text-primary); + font-size: 1.5rem; + cursor: pointer; +} + +.color-preview { + width: 100%; + height: 100px; + border-radius: 8px; + margin-bottom: 1.5rem; +} + +.color-values { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.color-values .input-group { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 1rem; + align-items: center; +} + +.color-values input { + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-secondary); + color: var(--text-primary); + font-family: monospace; +} + +.copy-button { + background: none; + border: none; + color: var(--text-primary); + cursor: pointer; + padding: 0.5rem; +} + +.copy-button:hover { + color: var(--primary-color); +} + +/* Input Styles */ +.input-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.input-group label { + display: flex; + justify-content: space-between; + color: var(--text-secondary); +} + +.input-group select { + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-primary); + color: var(--text-primary); +} + +.input-group input[type="range"] { + width: 100%; + height: 6px; + -webkit-appearance: none; + background: var(--border-color); + border-radius: 3px; + outline: none; +} + +.input-group input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; +} + +/* Button Styles */ +.primary-button, +.secondary-button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.primary-button { + background: var(--primary-color); + color: var(--text-on-primary); +} + +.primary-button:hover { + background: var(--primary-hover); +} + +.secondary-button { + background: var(--bg-hover); + color: var(--text-primary); +} + +.secondary-button:hover { + background: var(--border-color); +} + +.primary-button.small, +.secondary-button.small { + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .palette-controls { + padding: 1rem; + } + + .color-picker-container { + grid-template-columns: 1fr; + justify-items: center; + } + + .harmony-controls { + grid-template-columns: 1fr; + gap: 1rem; + } + + .palette-header { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .palette-actions { + justify-content: center; + } + + .color-values .input-group { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/css/components/forms.css b/css/components/forms.css new file mode 100644 index 0000000..ba59f23 --- /dev/null +++ b/css/components/forms.css @@ -0,0 +1,174 @@ +/* Form Container */ +.form-group { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + margin-bottom: var(--spacing-4); +} + +/* Form Labels */ +.form-label { + color: var(--text-primary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); +} + +/* Form Inputs */ +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: var(--spacing-2) var(--spacing-3); + color: var(--text-primary); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + transition: var(--transition-colors), var(--transition-shadow); +} + +.form-input:hover, +.form-select:hover, +.form-textarea:hover { + border-color: var(--border-hover); +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px var(--primary-alpha); +} + +/* Form Input Sizes */ +.form-input-sm, +.form-select-sm { + padding: var(--spacing-1) var(--spacing-2); + font-size: var(--font-size-sm); +} + +.form-input-lg, +.form-select-lg { + padding: var(--spacing-3) var(--spacing-4); + font-size: var(--font-size-lg); +} + +/* Form States */ +.form-input:disabled, +.form-select:disabled, +.form-textarea:disabled { + opacity: 0.6; + cursor: not-allowed; + background: var(--bg-disabled); +} + +.form-input.error, +.form-select.error, +.form-textarea.error { + border-color: var(--error-color); +} + +.form-input.error:focus, +.form-select.error:focus, +.form-textarea.error:focus { + box-shadow: 0 0 0 2px var(--error-alpha); +} + +/* Form Help Text */ +.form-help { + color: var(--text-secondary); + font-size: var(--font-size-sm); + line-height: var(--line-height-tight); +} + +.form-error { + color: var(--error-color); + font-size: var(--font-size-sm); + line-height: var(--line-height-tight); +} + +/* Form Checkbox and Radio */ +.form-check { + display: flex; + align-items: center; + gap: var(--spacing-2); + cursor: pointer; +} + +.form-checkbox, +.form-radio { + width: 16px; + height: 16px; + accent-color: var(--primary-color); + cursor: pointer; +} + +/* Form Layout */ +.form-row { + display: flex; + gap: var(--spacing-4); +} + +.form-col { + flex: 1; +} + +/* Form Validation */ +.form-input:invalid:not(:placeholder-shown), +.form-select:invalid:not(:placeholder-shown), +.form-textarea:invalid:not(:placeholder-shown) { + border-color: var(--error-color); +} + +/* Form Required */ +.form-required::after { + content: "*"; + color: var(--error-color); + margin-left: var(--spacing-1); +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .form-input, +[data-theme="dark"] .form-select, +[data-theme="dark"] .form-textarea { + background: var(--bg-tertiary); + border-color: var(--border-color); +} + +[data-theme="dark"] .form-input:disabled, +[data-theme="dark"] .form-select:disabled, +[data-theme="dark"] .form-textarea:disabled { + background: var(--bg-disabled-dark); +} + +/* Form File Upload */ +.form-file { + position: relative; + display: inline-block; +} + +.form-file-input { + position: absolute; + width: 0; + height: 0; + opacity: 0; +} + +.form-file-label { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-3); + color: var(--text-primary); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + transition: var(--transition-colors); +} + +.form-file-label:hover { + border-color: var(--border-hover); + background: var(--bg-hover); +} diff --git a/css/components/image-resizer.css b/css/components/image-resizer.css new file mode 100644 index 0000000..6e1e07e --- /dev/null +++ b/css/components/image-resizer.css @@ -0,0 +1,247 @@ +.resizer-controls { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Drop Zone */ +.drop-zone { + border: 2px dashed var(--border-color); + border-radius: 8px; + padding: 2rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + background: var(--bg-secondary); +} + +.drop-zone:hover, +.drop-zone.dragover { + border-color: var(--primary-color); + background: var(--bg-hover); +} + +.drop-zone-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.drop-zone-content i { + font-size: 3rem; + color: var(--primary-color); +} + +.file-types { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.file-input { + display: none; +} + +/* Preview Container */ +.preview-container { + margin-top: 2rem; + background: var(--bg-secondary); + border-radius: 8px; + padding: 1rem; +} + +.preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.file-info { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.preview-image-container { + width: 100%; + max-height: 400px; + overflow: hidden; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; +} + +.preview-image-container img { + max-width: 100%; + max-height: 400px; + object-fit: contain; +} + +/* Settings Panel */ +.settings-panel { + margin-top: 2rem; + background: var(--bg-secondary); + border-radius: 8px; + padding: 1.5rem; +} + +.settings-grid { + display: grid; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.dimension-controls { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 1rem; + align-items: center; +} + +.aspect-ratio-lock { + background: none; + border: none; + color: var(--text-primary); + cursor: pointer; + padding: 0.5rem; + font-size: 1.2rem; + transition: color 0.3s ease; +} + +.aspect-ratio-lock:hover { + color: var(--primary-color); +} + +.input-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.input-group label { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--text-secondary); +} + +.input-group input[type="number"], +.input-group select { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-primary); + color: var(--text-primary); +} + +.input-group input[type="range"] { + width: 100%; + height: 6px; + -webkit-appearance: none; + background: var(--border-color); + border-radius: 3px; + outline: none; +} + +.input-group input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +.primary-button, +.secondary-button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + transition: all 0.3s ease; +} + +.primary-button { + background: var(--primary-color); + color: var(--text-on-primary); +} + +.primary-button:hover { + background: var(--primary-hover); +} + +.secondary-button { + background: var(--bg-hover); + color: var(--text-primary); +} + +.secondary-button:hover { + background: var(--border-color); +} + +.secondary-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Notification */ +.notification { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 1rem 2rem; + border-radius: 4px; + background: var(--bg-secondary); + color: var(--text-primary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + display: none; + z-index: 1000; +} + +.notification.success { + background: var(--success-color); + color: white; +} + +.notification.error { + background: var(--error-color); + color: white; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .resizer-controls { + padding: 1rem; + } + + .dimension-controls { + grid-template-columns: 1fr; + } + + .aspect-ratio-lock { + justify-self: center; + } + + .action-buttons { + flex-direction: column; + } + + .primary-button, + .secondary-button { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/css/components/landing.css b/css/components/landing.css new file mode 100644 index 0000000..c9ddec0 --- /dev/null +++ b/css/components/landing.css @@ -0,0 +1,286 @@ +/* Landing Page Styles */ + +/* Hero Section */ +.hero { + background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); + padding: var(--spacing-20) 0 var(--spacing-12); + text-align: center; + position: relative; + overflow: hidden; +} + +.hero-content { + max-width: 800px; + margin: 0 auto; + padding: 0 var(--spacing-4); + position: relative; + z-index: var(--z-10); +} + +.hero-title { + font-size: var(--font-size-4xl); + font-weight: 700; + margin-bottom: var(--spacing-4); + background: linear-gradient(to right, var(--primary-color), var(--info-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-subtitle { + font-size: var(--font-size-xl); + color: var(--text-secondary); + margin-bottom: var(--spacing-8); + line-height: var(--line-height-relaxed); +} + +.hero-cta { + display: flex; + gap: var(--spacing-4); + justify-content: center; + margin-bottom: var(--spacing-12); +} + +.hero-features { + display: flex; + justify-content: center; + gap: var(--spacing-8); + flex-wrap: wrap; +} + +.feature-tag { + display: flex; + align-items: center; + gap: var(--spacing-2); + color: var(--text-secondary); + font-size: var(--font-size-sm); + font-weight: 500; +} + +.feature-tag i { + color: var(--success-color); +} + +/* Section Styles */ +.section-title { + text-align: center; + font-size: var(--font-size-3xl); + margin-bottom: var(--spacing-4); +} + +.section-subtitle { + text-align: center; + color: var(--text-secondary); + margin-bottom: var(--spacing-12); + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.features-section, +.about-section, +.contribute-section { + padding: var(--spacing-16) 0; +} + +.features-section { + background-color: var(--bg-secondary); +} + +/* Features Grid */ +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-8); + padding: 0 var(--spacing-4); +} + +.feature-card { + background: var(--bg-primary); + padding: var(--spacing-8); + border-radius: var(--radius-lg); + text-align: center; + transition: var(--transition-all); + border: 1px solid var(--border-color); +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); + border-color: var(--primary-color); +} + +.feature-card i { + font-size: 2.5rem; + color: var(--primary-color); + margin-bottom: var(--spacing-4); +} + +.feature-card h3 { + margin-bottom: var(--spacing-2); + color: var(--text-primary); +} + +.feature-card p { + color: var(--text-secondary); +} + +/* About Section */ +.about-content { + text-align: center; + max-width: 800px; + margin: 0 auto; + padding: 0 var(--spacing-4); +} + +.about-stats { + display: flex; + justify-content: center; + gap: var(--spacing-12); + margin-top: var(--spacing-12); + flex-wrap: wrap; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-number { + font-size: var(--font-size-4xl); + font-weight: 700; + color: var(--primary-color); + line-height: 1; + margin-bottom: var(--spacing-2); +} + +.stat-label { + color: var(--text-secondary); + font-weight: 500; +} + +/* Contribute Section */ +.contribute-content { + text-align: center; + max-width: 600px; + margin: 0 auto; + padding: 0 var(--spacing-4); +} + +.contribute-buttons { + display: flex; + justify-content: center; + gap: var(--spacing-4); + margin-top: var(--spacing-8); +} + +.contribute-button { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-3) var(--spacing-6); + border-radius: var(--radius-full); + font-weight: 600; + transition: var(--transition-all); +} + +.contribute-button.github { + background-color: #24292e; + color: white; +} + +.contribute-button.github:hover { + background-color: #1b1f23; + transform: translateY(-2px); +} + +.contribute-button.issues { + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + color: var(--text-primary); +} + +.contribute-button.issues:hover { + border-color: var(--primary-color); + color: var(--primary-color); + transform: translateY(-2px); +} + +/* Footer */ +.footer { + background-color: var(--bg-secondary); + border-top: 1px solid var(--border-color); + padding: var(--spacing-12) 0 var(--spacing-6); + margin-top: auto; +} + +.footer-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-4); + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: var(--spacing-12); + margin-bottom: var(--spacing-12); +} + +.footer-section h4 { + font-size: var(--font-size-lg); + font-weight: 600; + margin-bottom: var(--spacing-4); + color: var(--text-primary); +} + +.footer-section p { + color: var(--text-secondary); + line-height: var(--line-height-relaxed); +} + +.footer-section ul { + list-style: none; + padding: 0; +} + +.footer-section ul li { + margin-bottom: var(--spacing-2); +} + +.footer-section ul li a { + color: var(--text-secondary); + transition: var(--transition-colors); +} + +.footer-section ul li a:hover { + color: var(--primary-color); +} + +.footer-bottom { + text-align: center; + padding-top: var(--spacing-6); + border-top: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: var(--font-size-sm); +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .hero-title { + font-size: var(--font-size-3xl); + } + + .hero-features { + gap: var(--spacing-4); + } + + .footer-content { + grid-template-columns: 1fr; + gap: var(--spacing-8); + text-align: center; + } + + .contribute-buttons { + flex-direction: column; + } +} + diff --git a/css/components/modal.css b/css/components/modal.css new file mode 100644 index 0000000..f2d2d61 --- /dev/null +++ b/css/components/modal.css @@ -0,0 +1,158 @@ +/* Modal Wrapper */ +.modal-wrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: var(--z-50); + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-4); + opacity: 0; + visibility: hidden; + transition: var(--transition-opacity); +} + +.modal-wrapper.modal-show { + opacity: 1; + visibility: visible; +} + +/* Modal Overlay */ +.modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(var(--dark-bg-primary), 0.75); + -webkit-backdrop-filter: var(--backdrop-blur); + backdrop-filter: var(--backdrop-blur); +} + +/* Modal */ +.modal { + position: relative; + width: 100%; + max-width: 500px; + max-height: calc(100vh - var(--spacing-8)); + background: var(--bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + overflow: hidden; + transform: translateY(20px); + transition: var(--transition-transform); +} + +.modal-show .modal { + transform: translateY(0); +} + +/* Modal Sizes */ +.modal-small { + max-width: 400px; +} + +.modal-medium { + max-width: 600px; +} + +.modal-large { + max-width: 800px; +} + +.modal-full { + max-width: none; + margin: var(--spacing-4); +} + +/* Modal Header */ +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-4); + border-bottom: 1px solid var(--border-color); +} + +.modal-title { + margin: 0; + color: var(--text-primary); + font-size: var(--font-size-xl); + font-weight: 600; + line-height: var(--line-height-tight); +} + +/* Modal Close Button */ +.modal-close { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--radius-full); + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition-colors); +} + +.modal-close:hover { + color: var(--text-primary); + background: rgba(var(--dark-bg-tertiary), 0.1); +} + +.modal-close:focus { + outline: none; + box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color); +} + +/* Modal Body */ +.modal-body { + padding: var(--spacing-4); + color: var(--text-primary); + font-size: var(--font-size-base); + line-height: var(--line-height-normal); + overflow-y: auto; +} + +/* Modal Footer */ +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-2); + padding: var(--spacing-4); + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +/* Modal Buttons */ +.modal-footer .btn { + min-width: 80px; +} + +/* Mobile Styles */ +@media (max-width: 640px) { + .modal-wrapper { + padding: var(--spacing-2); + } + + .modal { + max-height: calc(100vh - var(--spacing-4)); + } + + .modal-header, + .modal-body, + .modal-footer { + padding: var(--spacing-3); + } + + .modal-title { + font-size: var(--font-size-lg); + } +} diff --git a/css/components/navigation.css b/css/components/navigation.css new file mode 100644 index 0000000..1173f7c --- /dev/null +++ b/css/components/navigation.css @@ -0,0 +1,155 @@ +/* Navigation */ +.nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: var(--z-40); + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); +} + +/* Navigation Container */ +.nav-container { + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; + padding: 0 var(--spacing-4); + max-width: 1280px; + margin: 0 auto; +} + +/* Navigation Brand */ +.nav-brand { + display: flex; + align-items: center; + gap: var(--spacing-2); + color: var(--text-primary); + font-size: var(--font-size-lg); + font-weight: 600; + text-decoration: none; +} + +.nav-brand:hover { + color: var(--primary-color); +} + +/* Navigation Menu */ +.nav-menu { + display: flex; + align-items: center; + gap: var(--spacing-6); + margin: 0; + padding: 0; + list-style: none; +} + +/* Navigation Link */ +.nav-link { + color: var(--text-secondary); + font-size: var(--font-size-sm); + font-weight: 500; + text-decoration: none; + transition: var(--transition-colors); +} + +.nav-link:hover, +.nav-link.active { + color: var(--text-primary); +} + +/* Navigation Actions */ +.nav-actions { + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +/* Navigation Toggle */ +.nav-toggle { + display: none; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--radius-md); + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition-colors); +} + +.nav-toggle:hover { + color: var(--text-primary); + background: var(--bg-secondary); +} + +.nav-toggle:focus { + outline: none; + box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color); +} + +/* Mobile Navigation */ +@media (max-width: 768px) { + .nav-toggle { + display: flex; + } + + .nav-menu { + position: fixed; + top: 64px; + left: 0; + right: 0; + flex-direction: column; + gap: 0; + height: calc(100vh - 64px); + padding: var(--spacing-4); + background: var(--bg-primary); + border-top: 1px solid var(--border-color); + transform: translateX(-100%); + transition: transform 0.3s ease-in-out; + } + + .nav-menu.active { + transform: translateX(0); + } + + .nav-link { + display: block; + padding: var(--spacing-3) 0; + font-size: var(--font-size-base); + } + + .nav-actions { + margin-top: var(--spacing-4); + padding-top: var(--spacing-4); + border-top: 1px solid var(--border-color); + } +} + +/* Transparent Navigation */ +.nav-transparent { + background: transparent; + border-bottom: none; + -webkit-backdrop-filter: var(--backdrop-blur) var(--backdrop-brightness) var(--backdrop-saturate); + backdrop-filter: var(--backdrop-blur) var(--backdrop-brightness) var(--backdrop-saturate); +} + +/* Scrolled Navigation */ +.nav-scrolled { + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .nav-toggle:hover { + background: rgba(var(--dark-bg-tertiary), 0.1); +} + +[data-theme="dark"] .nav-transparent { + background: rgba(var(--dark-bg-primary), 0.8); +} diff --git a/css/components/notifications.css b/css/components/notifications.css new file mode 100644 index 0000000..221e83a --- /dev/null +++ b/css/components/notifications.css @@ -0,0 +1,124 @@ +/* Notification Container */ +.notification-container { + position: fixed; + top: var(--spacing-4); + right: var(--spacing-4); + z-index: var(--z-50); + display: flex; + flex-direction: column; + gap: var(--spacing-2); + max-width: 100%; + width: 400px; + pointer-events: none; +} + +/* Notification */ +.notification { + position: relative; + display: flex; + align-items: center; + gap: var(--spacing-3); + padding: var(--spacing-4); + background: var(--bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + transform: translateX(120%); + transition: var(--transition-transform); + pointer-events: auto; + border-left: 4px solid transparent; +} + +.notification-show { + transform: translateX(0); +} + +/* Notification Types */ +.notification-info { + border-left-color: var(--info-color); + background: var(--info-color-light); +} + +.notification-success { + border-left-color: var(--success-color); + background: var(--success-color-light); +} + +.notification-warning { + border-left-color: var(--warning-color); + background: var(--warning-color-light); +} + +.notification-error { + border-left-color: var(--error-color); + background: var(--error-color-light); +} + +/* Notification Icon */ +.notification i { + font-size: var(--font-size-xl); +} + +.notification-info i { + color: var(--info-color); +} + +.notification-success i { + color: var(--success-color); +} + +.notification-warning i { + color: var(--warning-color); +} + +.notification-error i { + color: var(--error-color); +} + +/* Notification Content */ +.notification span { + flex: 1; + color: var(--text-primary); + font-size: var(--font-size-sm); + line-height: var(--line-height-normal); +} + +/* Close Button */ +.notification-close { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--radius-full); + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition-colors); +} + +.notification-close:hover { + color: var(--text-primary); + background: rgba(var(--dark-bg-tertiary), 0.1); +} + +.notification-close:focus { + outline: none; + box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color); +} + +/* Mobile Styles */ +@media (max-width: 640px) { + .notification-container { + top: auto; + bottom: var(--spacing-4); + left: var(--spacing-4); + right: var(--spacing-4); + width: auto; + } + + .notification { + padding: var(--spacing-3); + } +} diff --git a/css/components/password-generator.css b/css/components/password-generator.css new file mode 100644 index 0000000..0e21691 --- /dev/null +++ b/css/components/password-generator.css @@ -0,0 +1,326 @@ +.password-generator { + background: var(--card-bg); + border-radius: 10px; + padding: 2rem; + margin-top: 2rem; +} + +/* Password Output Section */ +.password-output { + margin-bottom: 2rem; +} + +.password-display { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.password-display input { + flex: 1; + padding: 1rem; + font-family: 'Roboto Mono', monospace; + font-size: 1.2rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + color: var(--text-light); + cursor: text; + transition: var(--transition); +} + +.password-display input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1); +} + +.password-actions { + display: flex; + gap: 0.5rem; +} + +/* Strength Meter */ +.strength-meter { + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + padding: 1rem; +} + +.strength-label { + color: var(--text-light); + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.strength-bars { + display: flex; + gap: 0.5rem; +} + +.strength-bar { + flex: 1; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + transition: var(--transition); +} + +.strength-bar.weak { + background: #ff4d4d; +} + +.strength-bar.medium { + background: #ffd700; +} + +.strength-bar.strong { + background: #00cc66; +} + +.strength-bar.very-strong { + background: #00ff00; +} + +/* Password Options */ +.password-options { + margin-bottom: 2rem; +} + +.option-group { + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + padding: 1.5rem; + margin-bottom: 1rem; +} + +.option-group h3 { + color: var(--text-light); + margin-bottom: 1rem; + font-size: 1.1rem; +} + +/* Length Control */ +.length-control { + display: flex; + align-items: center; + gap: 1rem; +} + +.length-control input[type="range"] { + flex: 1; + -webkit-appearance: none; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + outline: none; +} + +.length-control input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; + transition: var(--transition); +} + +.length-control input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +.length-control span { + min-width: 2.5rem; + text-align: center; + color: var(--text-light); + font-family: 'Roboto Mono', monospace; +} + +/* Checkbox Group */ +.checkbox-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-light); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + cursor: pointer; + transition: var(--transition); + position: relative; +} + +.checkbox-label input[type="checkbox"]:checked { + background: var(--primary-color); + border-color: var(--primary-color); +} + +.checkbox-label input[type="checkbox"]:checked::after { + content: '\2713'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--text-dark); + font-size: 14px; +} + +.checkbox-text { + font-size: 0.9rem; +} + +/* Advanced Options */ +.advanced-options { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.custom-chars { + margin-top: 0.5rem; +} + +.custom-chars input { + width: 100%; + padding: 0.75rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + color: var(--text-light); + font-family: 'Roboto Mono', monospace; + margin-top: 0.5rem; +} + +/* Password Requirements */ +.password-requirements { + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + padding: 1.5rem; + margin-bottom: 2rem; +} + +.requirements-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.requirements-list li { + color: var(--text-light); + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.requirements-list li i { + color: #ff4d4d; +} + +.requirements-list li.valid i { + color: #00cc66; +} + +/* Password History */ +.password-history { + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + padding: 1.5rem; +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.history-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; +} + +.history-item { + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: var(--transition); +} + +.history-item:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); +} + +.history-password { + font-family: 'Roboto Mono', monospace; + color: var(--text-light); + font-size: 0.9rem; +} + +.history-actions { + display: flex; + gap: 0.5rem; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .password-display { + flex-direction: column; + } + + .password-actions { + justify-content: flex-end; + } + + .checkbox-group { + grid-template-columns: 1fr; + } + + .requirements-list { + grid-template-columns: 1fr; + } + + .history-list { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/css/components/progress.css b/css/components/progress.css new file mode 100644 index 0000000..0c540c9 --- /dev/null +++ b/css/components/progress.css @@ -0,0 +1,158 @@ +/* Progress Container */ +.progress { + position: relative; + width: 100%; + height: 8px; + background: var(--bg-secondary); + border-radius: var(--radius-full); + overflow: hidden; +} + +/* Progress Bar */ +.progress-bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--gradient-primary); + border-radius: var(--radius-full); + transition: width 0.3s ease; +} + +/* Progress Sizes */ +.progress-sm { + height: 4px; +} + +.progress-lg { + height: 12px; +} + +/* Progress Variants */ +.progress-success .progress-bar { + background: var(--gradient-success); +} + +.progress-warning .progress-bar { + background: var(--gradient-warning); +} + +.progress-error .progress-bar { + background: var(--gradient-error); +} + +.progress-info .progress-bar { + background: var(--gradient-info); +} + +/* Progress with Label */ +.progress-labeled { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +.progress-label { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--text-secondary); + font-size: var(--font-size-sm); + line-height: var(--line-height-tight); +} + +/* Progress with Steps */ +.progress-steps { + display: flex; + justify-content: space-between; + margin-top: var(--spacing-2); +} + +.progress-step { + position: relative; + width: 16px; + height: 16px; + background: var(--bg-secondary); + border: 2px solid var(--border-color); + border-radius: var(--radius-full); + transition: var(--transition-colors); +} + +.progress-step.active { + background: var(--primary-color); + border-color: var(--primary-color); +} + +.progress-step.complete { + background: var(--success-color); + border-color: var(--success-color); +} + +/* Progress with Animation */ +.progress-animated .progress-bar { + animation: progress-animation 1s linear infinite; + background-size: 200% 100%; + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); +} + +@keyframes progress-animation { + from { + background-position: 200% 0; + } + to { + background-position: 0 0; + } +} + +/* Progress with Buffer */ +.progress-buffered { + position: relative; +} + +.progress-buffer { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--bg-tertiary); + border-radius: var(--radius-full); + transition: width 0.3s ease; +} + +/* Progress with Indeterminate State */ +.progress-indeterminate .progress-bar { + width: 50%; + animation: indeterminate 1.5s ease-in-out infinite; +} + +@keyframes indeterminate { + 0% { + left: -50%; + } + 100% { + left: 100%; + } +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .progress { + background: var(--bg-tertiary); +} + +[data-theme="dark"] .progress-step { + background: var(--bg-tertiary); + border-color: var(--border-color); +} + +[data-theme="dark"] .progress-buffer { + background: rgba(var(--dark-bg-tertiary), 0.3); +} diff --git a/css/components/qr-code.css b/css/components/qr-code.css new file mode 100644 index 0000000..f4b5109 --- /dev/null +++ b/css/components/qr-code.css @@ -0,0 +1,133 @@ +.qr-controls { + background: var(--card-bg); + border-radius: 10px; + padding: 2rem; + margin-top: 2rem; +} + +.input-group { + margin-bottom: 1.5rem; +} + +.input-group label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-light); +} + +.input-group input, +.input-group select, +.input-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + background: rgba(255, 255, 255, 0.05); + color: var(--text-light); + font-family: inherit; + transition: var(--transition); +} + +.input-group input:focus, +.input-group select:focus, +.input-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 255, 255, 0.1); +} + +.input-group input[type="color"] { + height: 40px; + padding: 0.25rem; + cursor: pointer; +} + +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.output-container { + margin-top: 2rem; + text-align: center; +} + +.qr-output { + background: white; + padding: 1rem; + border-radius: 10px; + display: inline-block; + margin-bottom: 1rem; + min-height: 256px; + min-width: 256px; +} + +.output-actions { + margin-top: 1rem; +} + +button { + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: var(--text-dark); + border: none; + padding: 0.75rem 1.5rem; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + transition: var(--transition); +} + +button:hover { + opacity: 0.9; + transform: translateY(-2px); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 1rem 2rem; + border-radius: 5px; + background: var(--card-bg); + color: var(--text-light); + display: none; + animation: slideIn 0.3s ease-out; + z-index: 1000; +} + +.notification.success { + background: #28a745; +} + +.notification.error { + background: #dc3545; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@media (max-width: 768px) { + .settings-grid { + grid-template-columns: 1fr; + } + + .qr-output { + max-width: 100%; + } +} \ No newline at end of file diff --git a/css/components/social-share.css b/css/components/social-share.css new file mode 100644 index 0000000..039ab8c --- /dev/null +++ b/css/components/social-share.css @@ -0,0 +1,93 @@ +.social-share { + margin: 2rem 0; + padding: 1.5rem; + background: var(--bg-secondary); + border-radius: 8px; +} + +.social-share h3 { + margin-bottom: 1rem; + color: var(--text-primary); +} + +.share-buttons { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.share-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.share-button i { + font-size: 1.2rem; +} + +/* Twitter */ +.share-button.twitter { + background: #1DA1F2; + color: white; +} + +.share-button.twitter:hover { + background: #0d8ed9; +} + +/* Facebook */ +.share-button.facebook { + background: #4267B2; + color: white; +} + +.share-button.facebook:hover { + background: #365899; +} + +/* Reddit */ +.share-button.reddit { + background: #FF4500; + color: white; +} + +.share-button.reddit:hover { + background: #e63e00; +} + +/* Copy Link */ +.share-button.copy-link { + background: var(--bg-hover); + color: var(--text-primary); +} + +.share-button.copy-link:hover { + background: var(--border-color); +} + +/* Success State */ +.share-button.copy-link.success { + background: var(--success-color); + color: white; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .share-buttons { + flex-direction: column; + } + + .share-button { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/css/components/spinner.css b/css/components/spinner.css new file mode 100644 index 0000000..ffd79a7 --- /dev/null +++ b/css/components/spinner.css @@ -0,0 +1,66 @@ +/* Loading Spinner Container */ +.loading-spinner { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-3); +} + +/* Spinner Inner */ +.loading-spinner-inner { + width: 40px; + height: 40px; + border: 3px solid var(--bg-secondary); + border-top-color: var(--primary-color); + border-radius: var(--radius-full); + animation: spinner 0.8s linear infinite; +} + +/* Spinner Sizes */ +.loading-spinner-small .loading-spinner-inner { + width: 24px; + height: 24px; + border-width: 2px; +} + +.loading-spinner-large .loading-spinner-inner { + width: 56px; + height: 56px; + border-width: 4px; +} + +/* Spinner Text */ +.loading-spinner-text { + color: var(--text-secondary); + font-size: var(--font-size-sm); + line-height: var(--line-height-tight); +} + +/* Spinner Animation */ +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +/* Spinner Overlay */ +.loading-spinner-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: var(--z-50); + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(var(--dark-bg-primary), 0.75); + -webkit-backdrop-filter: var(--backdrop-blur); + backdrop-filter: var(--backdrop-blur); +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .loading-spinner-inner { + border-color: var(--bg-tertiary); + border-top-color: var(--primary-color); +} diff --git a/css/components/text-to-speech.css b/css/components/text-to-speech.css new file mode 100644 index 0000000..b1773e0 --- /dev/null +++ b/css/components/text-to-speech.css @@ -0,0 +1,598 @@ +/** + * Text-to-Speech Component Styles + */ + +.tts-container { + background: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--spacing-lg); + box-shadow: var(--shadow-md); +} + +.tts-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-md); +} + +.tts-title { + font-size: 1.25rem; + color: var(--text-primary); +} + +.tts-actions { + display: flex; + gap: var(--spacing-sm); +} + +.tts-textarea { + width: 100%; + min-height: 200px; + padding: var(--spacing-md); + margin-bottom: var(--spacing-md); + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + color: var(--text-primary); + font-family: var(--font-mono); + resize: vertical; +} + +.tts-info { + display: flex; + justify-content: space-between; + margin-bottom: var(--spacing-md); + color: var(--text-muted); + font-size: 0.875rem; +} + +.tts-controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.tts-control { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.tts-control label { + color: var(--text-primary); + font-weight: 500; +} + +.tts-control select, +.tts-control input[type="range"] { + width: 100%; + padding: var(--spacing-sm); + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + color: var(--text-primary); +} + +.tts-control input[type="range"] { + -webkit-appearance: none; + height: 6px; + background: var(--primary-color); + border: none; + border-radius: 3px; +} + +.tts-control input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: var(--primary-color); + border: 2px solid var(--bg-dark); + border-radius: 50%; + cursor: pointer; +} + +.tts-buttons { + display: flex; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); +} + +.tts-button { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--primary-color); + border: none; + border-radius: var(--border-radius); + color: var(--text-light); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.tts-button:hover { + background: var(--primary-dark); +} + +.tts-button.secondary { + background: var(--bg-dark); + border: 1px solid var(--border-color); +} + +.tts-button.secondary:hover { + background: var(--bg-darker); +} + +.tts-templates { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: var(--spacing-md); + margin-top: var(--spacing-xl); + padding-top: var(--spacing-lg); + border-top: 1px solid var(--border-color); +} + +.template-btn { + padding: var(--spacing-sm); + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + color: var(--text-primary); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; +} + +.template-btn:hover { + background: var(--bg-darker); + transform: translateY(-1px); +} + +.template-btn i { + margin-right: var(--spacing-sm); + color: var(--primary-color); +} + +@media (max-width: 768px) { + .tts-controls { + grid-template-columns: 1fr; + } + + .tts-buttons { + flex-direction: column; + } + + .tts-templates { + grid-template-columns: 1fr 1fr; + } +} + +/* Text-to-Speech Tool Styles */ + +/* Tool Interface Content */ +.tool-interface-content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +/* Text Input Container */ +.text-input-container { + position: relative; +} + +.text-area { + width: 100%; + min-height: 200px; + padding: 1rem; + border-radius: var(--radius-md); + background: var(--bg-primary); + border: 1px solid rgba(var(--bg-secondary-rgb), 0.5); + color: var(--text-primary); + font-size: 1rem; + line-height: 1.5; + resize: vertical; + transition: var(--transition-all); +} + +.text-area:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2); +} + +.text-area::placeholder { + color: var(--text-secondary); +} + +/* Controls */ +.controls { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.control-button { + padding: 0.75rem 1.5rem; + border-radius: var(--radius-md); + border: none; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 1rem; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: var(--transition-all); + border: 1px solid rgba(var(--bg-secondary-rgb), 0.5); +} + +.control-button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: rgba(var(--primary-rgb), 0.2); +} + +.control-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.control-button.primary { + background: var(--primary-color); + color: white; + border: none; +} + +.control-button.primary:hover:not(:disabled) { + background: var(--primary-color-dark); +} + +.control-button i { + font-size: 1.2rem; +} + +/* Options */ +.option-group { + margin-bottom: 1.5rem; +} + +.option-group:last-child { + margin-bottom: 0; +} + +.option-group label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-primary); + font-weight: 500; +} + +.select-input { + width: 100%; + padding: 0.75rem; + border-radius: var(--radius-md); + background: var(--bg-primary); + border: 1px solid rgba(var(--bg-secondary-rgb), 0.5); + color: var(--text-primary); + font-size: 1rem; + transition: var(--transition-all); +} + +.select-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2); +} + +.select-input optgroup { + font-weight: 600; + color: var(--text-secondary); +} + +.select-input option { + padding: 0.5rem; + color: var(--text-primary); +} + +.range-input { + width: 100%; + height: 6px; + -webkit-appearance: none; + background: rgba(var(--bg-secondary-rgb), 0.5); + border-radius: var(--radius-full); + outline: none; + transition: var(--transition-all); +} + +.range-input::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--primary-color); + cursor: pointer; + transition: var(--transition-all); +} + +.range-input::-moz-range-thumb { + width: 18px; + height: 18px; + border: none; + border-radius: 50%; + background: var(--primary-color); + cursor: pointer; + transition: var(--transition-all); +} + +.range-input:hover::-webkit-slider-thumb { + transform: scale(1.2); +} + +.range-input:hover::-moz-range-thumb { + transform: scale(1.2); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .controls { + flex-direction: column; + } + + .control-button { + width: 100%; + justify-content: center; + } +} + +/* Text to Speech Tool */ +.text-to-speech { + display: flex; + flex-direction: column; + gap: 2rem; +} + +/* Input Section */ +.text-input-section { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: 1.5rem; + box-shadow: var(--shadow-md); +} + +.text-input-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.text-input-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary); +} + +.text-input-controls { + display: flex; + gap: 0.5rem; +} + +.text-input-textarea { + width: 100%; + min-height: 200px; + padding: 1rem; + border: 1px solid rgba(var(--text-primary-rgb), 0.1); + border-radius: var(--radius-md); + background: var(--bg-secondary); + color: var(--text-primary); + font-family: inherit; + font-size: 1rem; + line-height: 1.5; + resize: vertical; + transition: var(--transition-all); +} + +.text-input-textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1); +} + +.text-input-textarea::placeholder { + color: var(--text-secondary); +} + +/* Voice Options */ +.voice-options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.voice-option { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.voice-option label { + font-weight: 500; + color: var(--text-primary); +} + +.voice-select { + padding: 0.75rem 1rem; + border: 1px solid rgba(var(--text-primary-rgb), 0.1); + border-radius: var(--radius-md); + background: var(--bg-secondary); + color: var(--text-primary); + font-family: inherit; + transition: var(--transition-all); +} + +.voice-select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1); +} + +/* Speech Controls */ +.speech-controls { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.speech-control { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.speech-control label { + font-weight: 500; + color: var(--text-primary); +} + +.speech-control input[type="range"] { + width: 100px; + height: 4px; + background: rgba(var(--primary-rgb), 0.2); + border-radius: var(--radius-full); + -webkit-appearance: none; +} + +.speech-control input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; + transition: var(--transition-all); +} + +.speech-control input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.action-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border-radius: var(--radius-md); + font-weight: 500; + transition: var(--transition-all); + cursor: pointer; + border: none; +} + +.action-button i { + font-size: 1.25rem; +} + +.action-button.play { + background: var(--primary-color); + color: var(--text-light); +} + +.action-button.play:hover { + background: var(--primary-color-dark); + transform: translateY(-2px); +} + +.action-button.stop { + background: rgba(var(--error-rgb), 0.1); + color: var(--error-color); +} + +.action-button.stop:hover { + background: rgba(var(--error-rgb), 0.2); + transform: translateY(-2px); +} + +.action-button.pause { + background: rgba(var(--warning-rgb), 0.1); + color: var(--warning-color); +} + +.action-button.pause:hover { + background: rgba(var(--warning-rgb), 0.2); + transform: translateY(-2px); +} + +.action-button.resume { + background: rgba(var(--success-rgb), 0.1); + color: var(--success-color); +} + +.action-button.resume:hover { + background: rgba(var(--success-rgb), 0.2); + transform: translateY(-2px); +} + +/* Progress Bar */ +.speech-progress { + margin-top: 1rem; +} + +.progress-bar { + height: 4px; + background: rgba(var(--primary-rgb), 0.2); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background: var(--primary-color); + border-radius: var(--radius-full); + transition: width 0.3s ease; +} + +.progress-text { + display: flex; + justify-content: space-between; + margin-top: 0.5rem; + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .voice-options { + grid-template-columns: 1fr; + } + + .speech-controls { + flex-direction: column; + } + + .speech-control { + width: 100%; + } + + .speech-control input[type="range"] { + flex: 1; + } + + .action-buttons { + flex-direction: column; + } + + .action-button { + width: 100%; + justify-content: center; + } +} diff --git a/css/components/tool-page.css b/css/components/tool-page.css new file mode 100644 index 0000000..c449c77 --- /dev/null +++ b/css/components/tool-page.css @@ -0,0 +1,275 @@ +/* Tool Page Layout */ +.tool-page { + padding-top: 4rem; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.tool-header { + background: var(--surface-gradient); + padding: 4rem 2rem; + text-align: center; +} + +.tool-title { + font-size: 2.5rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.tool-description { + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto 2rem; +} + +.tool-features { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: center; + margin-bottom: 2rem; +} + +.tool-feature { + background: rgba(var(--bg-secondary-rgb), 0.5); + color: var(--text-primary); + padding: 0.5rem 1rem; + border-radius: var(--radius-full); + font-size: 0.875rem; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.tool-content { + flex: 1; + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +/* Tool Controls */ +.tool-controls { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: var(--shadow-md); +} + +.tool-controls-title { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.tool-controls-group { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +@media (max-width: 768px) { + .tool-controls-group { + flex-direction: column; + } +} + +/* Tool Output */ +.tool-output { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: var(--shadow-md); +} + +.tool-output-title { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-primary); + display: flex; + justify-content: space-between; + align-items: center; +} + +.tool-output-content { + background: var(--bg-secondary); + border-radius: var(--radius-md); + padding: 1rem; + margin-bottom: 1rem; + max-height: 400px; + overflow-y: auto; +} + +.tool-output-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +/* Tool Settings */ +.tool-settings { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: var(--shadow-md); +} + +.tool-settings-title { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.tool-settings-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +/* Tool History */ +.tool-history { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: var(--shadow-md); +} + +.tool-history-title { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.tool-history-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.tool-history-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: var(--radius-md); + color: var(--text-primary); +} + +.tool-history-item:hover { + background: rgba(var(--bg-secondary-rgb), 0.8); +} + +/* Tool Footer */ +.tool-footer { + background: var(--bg-secondary); + padding: 2rem; + margin-top: auto; +} + +.tool-footer-content { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +@media (max-width: 768px) { + .tool-header { + padding: 3rem 1rem; + } + + .tool-title { + font-size: 2rem; + } + + .tool-content { + padding: 1rem; + } + + .tool-footer-content { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} + +/* Tool Loading State */ +.tool-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.tool-loading-text { + margin-top: 1rem; + color: var(--text-secondary); +} + +/* Tool Error State */ +.tool-error { + background: rgba(var(--error-rgb), 0.1); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + color: var(--error-color); + display: flex; + align-items: center; + gap: 1rem; +} + +.tool-error-icon { + font-size: 1.5rem; +} + +.tool-error-content { + flex: 1; +} + +.tool-error-title { + font-weight: 600; + margin-bottom: 0.5rem; +} + +.tool-error-message { + font-size: 0.875rem; +} + +/* Tool Success State */ +.tool-success { + background: rgba(var(--success-rgb), 0.1); + border-radius: var(--radius-lg); + padding: 1.5rem; + margin-bottom: 2rem; + color: var(--success-color); + display: flex; + align-items: center; + gap: 1rem; +} + +.tool-success-icon { + font-size: 1.5rem; +} + +.tool-success-content { + flex: 1; +} + +.tool-success-title { + font-weight: 600; + margin-bottom: 0.5rem; +} + +.tool-success-message { + font-size: 0.875rem; +} diff --git a/css/components/tooltip.css b/css/components/tooltip.css new file mode 100644 index 0000000..c7dd0d1 --- /dev/null +++ b/css/components/tooltip.css @@ -0,0 +1,62 @@ +/* Tooltip */ +.tooltip { + position: fixed; + z-index: var(--z-50); + padding: var(--spacing-2) var(--spacing-3); + background: var(--bg-tertiary); + color: var(--text-light); + font-size: var(--font-size-sm); + line-height: var(--line-height-tight); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + pointer-events: none; + opacity: 0; + transform-origin: center; + transform: scale(0.9); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +.tooltip-show { + opacity: 1; + transform: scale(1); +} + +/* Tooltip Positions */ +.tooltip::before { + content: ''; + position: absolute; + width: 8px; + height: 8px; + background: inherit; + transform: rotate(45deg); +} + +.tooltip-top::before { + bottom: -4px; + left: 50%; + margin-left: -4px; +} + +.tooltip-bottom::before { + top: -4px; + left: 50%; + margin-left: -4px; +} + +.tooltip-left::before { + right: -4px; + top: 50%; + margin-top: -4px; +} + +.tooltip-right::before { + left: -4px; + top: 50%; + margin-top: -4px; +} + +/* Dark Theme Adjustments */ +[data-theme="dark"] .tooltip { + background: var(--bg-secondary); + box-shadow: var(--shadow-xl); +} diff --git a/css/components/ui.css b/css/components/ui.css new file mode 100644 index 0000000..bcb5ff8 --- /dev/null +++ b/css/components/ui.css @@ -0,0 +1,433 @@ +/** + * Common UI Components + */ + +/* Buttons */ +.button, +.tech-button, +.primary-button, +.secondary-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + border: none; + border-radius: var(--border-radius); + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; +} + +.button i, +.tech-button i, +.primary-button i, +.secondary-button i { + font-size: 0.9em; +} + +.primary-button { + background-color: var(--primary-color); + color: var(--text-light); +} + +.primary-button:hover { + background-color: var(--primary-dark); +} + +.secondary-button { + background-color: var(--secondary-color); + color: var(--text-light); +} + +.secondary-button:hover { + background-color: var(--secondary-dark); +} + +.button.small, +.tech-button.small { + padding: var(--spacing-xs) var(--spacing-sm); + font-size: 0.9em; +} + +/* Form Controls */ +.input-group { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + margin-bottom: var(--spacing-md); +} + +.input-group label { + font-weight: 500; + color: var(--text-primary); +} + +input[type="text"], +input[type="number"], +input[type="url"], +input[type="email"], +input[type="password"], +textarea, +select { + padding: var(--spacing-sm); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background-color: var(--input-bg); + color: var(--text-primary); + font-size: 1rem; + transition: border-color 0.3s ease; +} + +input:focus, +textarea:focus, +select:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0 0 0 2px var(--primary-light); +} + +/* Progress & Loading */ +.progress-bar { + width: 100%; + height: 4px; + background-color: var(--bg-light); + border-radius: var(--border-radius); + overflow: hidden; +} + +.progress-bar .progress { + height: 100%; + background-color: var(--primary-color); + transition: width 0.3s ease; +} + +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid var(--bg-light); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +/* Navigation */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 4rem; + background: rgba(var(--bg-primary-rgb), 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-bottom: 1px solid rgba(var(--bg-secondary-rgb), 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; +} + +.nav-container .logo { + font-family: 'Orbitron', sans-serif; + font-size: 1.5rem; + font-weight: 700; +} + +.nav-links { + display: flex; + align-items: center; + gap: 2rem; +} + +.nav-link { + color: var(--text-primary); + font-weight: 500; + transition: var(--transition-all); +} + +.nav-link:hover { + color: var(--primary-color); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + border-radius: var(--radius-md); + font-weight: 500; + transition: var(--transition-all); + cursor: pointer; + border: none; + gap: 0.5rem; +} + +.btn-primary { + background: var(--primary-color); + color: var(--text-light); +} + +.btn-primary:hover { + background: var(--primary-color-dark); + transform: translateY(-2px); +} + +.btn-secondary { + background: rgba(var(--bg-secondary-rgb), 0.5); + color: var(--text-primary); +} + +.btn-secondary:hover { + background: rgba(var(--bg-secondary-rgb), 0.8); + transform: translateY(-2px); +} + +.btn-outline { + background: transparent; + border: 1px solid var(--primary-color); + color: var(--primary-color); +} + +.btn-outline:hover { + background: var(--primary-color); + color: var(--text-light); + transform: translateY(-2px); +} + +/* Form Controls */ +.form-control { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.form-label { + font-weight: 500; + color: var(--text-primary); +} + +.form-input { + padding: 0.75rem 1rem; + border-radius: var(--radius-md); + border: 1px solid rgba(var(--text-primary-rgb), 0.1); + background: var(--bg-primary); + color: var(--text-primary); + transition: var(--transition-all); +} + +.form-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1); +} + +.form-input::placeholder { + color: var(--text-secondary); +} + +/* Progress */ +.progress { + width: 100%; + height: 0.5rem; + background: rgba(var(--bg-secondary-rgb), 0.5); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: var(--primary-color); + border-radius: var(--radius-full); + transition: width 0.3s ease; +} + +/* Cards */ +.card { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: 1.5rem; + box-shadow: var(--shadow-md); + transition: var(--transition-all); +} + +.card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.card-header { + margin-bottom: 1rem; +} + +.card-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.card-subtitle { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.card-body { + color: var(--text-primary); +} + +.card-footer { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(var(--text-primary-rgb), 0.1); +} + +/* Alerts */ +.alert { + padding: 1rem; + border-radius: var(--radius-md); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.alert-success { + background: rgba(var(--success-rgb), 0.1); + color: var(--success-color); +} + +.alert-warning { + background: rgba(var(--warning-rgb), 0.1); + color: var(--warning-color); +} + +.alert-error { + background: rgba(var(--error-rgb), 0.1); + color: var(--error-color); +} + +.alert-info { + background: rgba(var(--info-rgb), 0.1); + color: var(--info-color); +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border-radius: var(--radius-full); + font-size: 0.75rem; + font-weight: 500; +} + +.badge-primary { + background: rgba(var(--primary-rgb), 0.1); + color: var(--primary-color); +} + +.badge-success { + background: rgba(var(--success-rgb), 0.1); + color: var(--success-color); +} + +.badge-warning { + background: rgba(var(--warning-rgb), 0.1); + color: var(--warning-color); +} + +.badge-error { + background: rgba(var(--error-rgb), 0.1); + color: var(--error-color); +} + +/* Tooltips */ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip:hover::before { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: 0.5rem 1rem; + background: rgba(var(--bg-secondary-rgb), 0.9); + color: var(--text-primary); + font-size: 0.875rem; + border-radius: var(--radius-md); + white-space: nowrap; + z-index: 1000; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +/* Loading Spinner */ +.spinner { + width: 2rem; + height: 2rem; + border: 2px solid rgba(var(--primary-rgb), 0.1); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Mobile Navigation */ +.mobile-menu-button { + display: none; + background: none; + border: none; + color: var(--text-primary); + font-size: 1.5rem; + cursor: pointer; + padding: 0.5rem; + border-radius: var(--radius-md); + transition: var(--transition-all); +} + +.mobile-menu-button:hover { + background: rgba(var(--bg-secondary-rgb), 0.5); +} + +@media (max-width: 768px) { + .nav-container { + padding: 0 1rem; + } + + .mobile-menu-button { + display: block; + } + + .nav-links { + display: none; + position: fixed; + top: 4rem; + left: 0; + right: 0; + background: var(--bg-primary); + padding: 1rem; + flex-direction: column; + gap: 1rem; + border-bottom: 1px solid rgba(var(--bg-secondary-rgb), 0.5); + } + + .nav-links.active { + display: flex; + } + + .btn { + width: 100%; + } +} diff --git a/css/components/url-shortener.css b/css/components/url-shortener.css new file mode 100644 index 0000000..0ef5907 --- /dev/null +++ b/css/components/url-shortener.css @@ -0,0 +1,309 @@ +.url-shortener { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.input-section { + margin-bottom: 2rem; +} + +.url-input-container { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.url-input-container input { + flex: 1; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 1rem; + transition: var(--transition); +} + +.url-input-container input:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2); +} + +.options-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +.custom-alias { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.custom-alias input { + padding: 0.75rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 0.9rem; +} + +.hint { + font-size: 0.8rem; + color: var(--text-light); + opacity: 0.7; +} + +.expiry-options { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.expiry-options select, +.expiry-options input[type="date"] { + padding: 0.75rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 0.9rem; +} + +.result-section { + padding: 2rem; + background: rgba(var(--primary-color-rgb), 0.1); + border-radius: 10px; + margin-top: 2rem; +} + +.shortened-url-container { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.shortened-url-container input { + flex: 1; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 1rem; + cursor: text; +} + +.stats-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-light); + opacity: 0.7; + margin-bottom: 0.5rem; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: var(--background-light); + padding: 2rem; + border-radius: 10px; + max-width: 400px; + width: 90%; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.close-button { + background: none; + border: none; + font-size: 1.5rem; + color: var(--text-light); + cursor: pointer; + padding: 0.5rem; +} + +#qr-code { + display: flex; + justify-content: center; + margin: 2rem 0; +} + +.features-section { + margin-top: 4rem; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.feature-card { + background: rgba(255, 255, 255, 0.05); + padding: 2rem; + border-radius: 10px; + text-align: center; + transition: var(--transition); +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); +} + +.feature-card i { + font-size: 2rem; + color: var(--primary-color); + margin-bottom: 1rem; +} + +.feature-card h3 { + margin-bottom: 1rem; + color: var(--text-light); +} + +.feature-card p { + color: var(--text-light); + opacity: 0.8; + font-size: 0.9rem; +} + +.hidden { + display: none; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .url-input-container { + flex-direction: column; + } + + .options-container { + grid-template-columns: 1fr; + } + + .stats-container { + grid-template-columns: 1fr; + gap: 1rem; + } + + .shortened-url-container { + flex-direction: column; + } + + .shortened-url-container button { + width: 100%; + } +} + +.history-section { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.history-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.history-item { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 1rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 5px; + align-items: center; +} + +.history-item .url-info { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.history-item .short-url { + font-weight: 700; + color: var(--primary-color); +} + +.history-item .long-url { + font-size: 0.9rem; + color: var(--text-light); + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.history-item .meta-info { + font-size: 0.8rem; + color: var(--text-light); + opacity: 0.7; +} + +.history-item .action-buttons { + display: flex; + gap: 0.5rem; +} + +@media (max-width: 768px) { + .history-item { + grid-template-columns: 1fr; + } + + .history-item .action-buttons { + justify-content: flex-end; + } +} \ No newline at end of file diff --git a/css/image.resizer.css b/css/image.resizer.css deleted file mode 100644 index 4eb9685..0000000 --- a/css/image.resizer.css +++ /dev/null @@ -1,11 +0,0 @@ -#image-container { - max-height: 300px; - overflow: hidden; - margin-bottom: 1rem; - border-radius: 5px; -} - -#image-container img { - max-width: 100%; - height: auto; -} diff --git a/css/styles.css b/css/styles.css index d90dd25..bf94e8a 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,140 +1,214 @@ -body { - font-family: 'Roboto', sans-serif; - background-color: #000; - color: #fff; - margin: 0; - padding: 0; - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; +/* Base Styles */ +@import url('base/reset.css'); +@import url('base/layout.css'); + +/* Theme */ +@import url('themes/variables.css'); + +/* Components */ +@import url('components/ui.css'); +@import url('components/navigation.css'); +@import url('components/buttons.css'); +@import url('components/forms.css'); +@import url('components/modal.css'); +@import url('components/landing.css'); +@import url('components/cards.css'); +@import url('components/tool-page.css'); +@import url('components/text-to-speech.css'); + +/* Utils */ +@import url('utils/utilities.css'); +@import url('utils/animations.css'); + +/* Global Styles */ +html { + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 16px; + line-height: 1.5; + scroll-behavior: smooth; } -.container { - background-color: rgba(16, 24, 39, 0.8); - padding: 2rem; - border-radius: 10px; - box-shadow: 0 0 20px #00fff2; - max-width: 600px; - width: 100%; +body { + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + display: flex; + flex-direction: column; + padding-top: 4rem; } -.neon-text { +/* Typography */ +h1, h2, h3, h4, h5, h6 { font-family: 'Orbitron', sans-serif; - color: #fff; - text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #00fff2, 0 0 35px #00fff2, 0 0 40px #00fff2; - text-align: center; - margin-bottom: 2rem; + font-weight: 700; + line-height: 1.2; + margin-bottom: 1rem; } -nav { - display: flex; - justify-content: center; - margin-bottom: 2rem; +h1 { + font-size: 3rem; } -nav a { - color: #00fff2; - text-decoration: none; - margin: 0 1rem; - font-weight: bold; - transition: color 0.3s ease; +h2 { + font-size: 2.5rem; } -nav a:hover { - color: #fff; - text-shadow: 0 0 5px #00fff2; +h3 { + font-size: 2rem; } -.content { - display: flex; - flex-direction: column; - gap: 1rem; +h4 { + font-size: 1.5rem; } -.input-group { - display: flex; - flex-direction: column; +h5 { + font-size: 1.25rem; } -label { - margin-bottom: 0.5rem; - color: #00fff2; +h6 { + font-size: 1rem; } -input[type="file"], input[type="number"] { - background-color: #1f2937; - border: 1px solid #374151; - color: #fff; - padding: 0.5rem; - border-radius: 5px; +p { + margin-bottom: 1rem; } -.tech-button { - background: linear-gradient(45deg, #00a3ff, #00fff2); - color: #000; - border: none; - padding: 10px 20px; - font-size: 16px; - font-weight: bold; - text-transform: uppercase; - border-radius: 5px; - cursor: pointer; - transition: all 0.3s ease; - text-align: center; +a { + color: var(--primary-color); text-decoration: none; + transition: var(--transition-all); } -.tech-button:hover { - background: linear-gradient(45deg, #00fff2, #00a3ff); - box-shadow: 0 0 10px #00fff2; +a:hover { + color: var(--primary-color-dark); } -.instructions { - text-align: center; - color: #9ca3af; - font-size: 0.9rem; +/* Container */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Navigation */ +.nav-container { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 4rem; + background: rgba(var(--bg-primary-rgb), 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-bottom: 1px solid rgba(var(--bg-secondary-rgb), 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; } -.welcome-text { - font-size: 1.2rem; - text-align: center; - margin-bottom: 1rem; +.nav-container .logo { + font-family: 'Orbitron', sans-serif; + font-size: 1.5rem; + font-weight: 700; } -.services-list { - list-style-type: none; - padding: 0; +.nav-links { + display: flex; + align-items: center; + gap: 2rem; } -.services-list li { - margin-bottom: 1rem; +.nav-link { + color: var(--text-primary); + font-weight: 500; + transition: var(--transition-all); } -.services-list a { - color: #00fff2; - text-decoration: none; - font-weight: bold; - transition: color 0.3s ease; +.nav-link:hover { + color: var(--primary-color); } -.services-list a:hover { - color: #fff; - text-shadow: 0 0 5px #00fff2; +.theme-toggle { + display: flex; + align-items: center; } -.cta-text { - text-align: center; - margin-top: 2rem; +.theme-toggle button { + background: none; + border: none; + color: var(--text-primary); + font-size: 1.25rem; + cursor: pointer; + padding: 0.5rem; + border-radius: var(--radius-full); + transition: var(--transition-all); } -.cta-text a { - color: #00fff2; - text-decoration: none; - font-weight: bold; +.theme-toggle button:hover { + background: rgba(var(--bg-secondary-rgb), 0.5); + transform: rotate(15deg); } -.cta-text a:hover { - color: #fff; - text-shadow: 0 0 5px #00fff2; +/* Mobile Navigation */ +.mobile-menu-button { + display: none; + background: none; + border: none; + color: var(--text-primary); + font-size: 1.5rem; + cursor: pointer; + padding: 0.5rem; + border-radius: var(--radius-md); + transition: var(--transition-all); +} + +.mobile-menu-button:hover { + background: rgba(var(--bg-secondary-rgb), 0.5); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .nav-container { + padding: 0 1rem; + } + + .nav-links { + display: none; + position: fixed; + top: 4rem; + left: 0; + right: 0; + background: var(--bg-primary); + padding: 1rem; + flex-direction: column; + gap: 1rem; + border-bottom: 1px solid rgba(var(--bg-secondary-rgb), 0.5); + } + + .nav-links.active { + display: flex; + } + + .mobile-menu-button { + display: block; + } + + h1 { + font-size: 2.5rem; + } + + h2 { + font-size: 2rem; + } + + h3 { + font-size: 1.75rem; + } + + h4 { + font-size: 1.5rem; + } } diff --git a/css/themes/variables.css b/css/themes/variables.css new file mode 100644 index 0000000..76b55c2 --- /dev/null +++ b/css/themes/variables.css @@ -0,0 +1,197 @@ +/** + * CSS Variables and Theming + */ + +:root { + /* Base Colors */ + --primary-rgb: 59, 130, 246; + --success-rgb: 34, 197, 94; + --warning-rgb: 234, 179, 8; + --error-rgb: 239, 68, 68; + --info-rgb: 59, 130, 246; + + /* Light Theme Colors */ + --light-bg-primary: 255, 255, 255; + --light-bg-secondary: 249, 250, 251; + --light-bg-tertiary: 243, 244, 246; + --light-text-primary: 17, 24, 39; + --light-text-secondary: 107, 114, 128; + --light-border: 229, 231, 235; + --light-shadow: 0, 0, 0; + + /* Dark Theme Colors */ + --dark-bg-primary: 17, 24, 39; + --dark-bg-secondary: 31, 41, 55; + --dark-bg-tertiary: 55, 65, 81; + --dark-text-primary: 243, 244, 246; + --dark-text-secondary: 156, 163, 175; + --dark-border: 75, 85, 99; + --dark-shadow: 0, 0, 0; +} + +/* Light Theme */ +[data-theme="light"] { + /* Colors */ + --bg-primary: rgb(var(--light-bg-primary)); + --bg-secondary: rgb(var(--light-bg-secondary)); + --bg-tertiary: rgb(var(--light-bg-tertiary)); + --text-primary: rgb(var(--light-text-primary)); + --text-secondary: rgb(var(--light-text-secondary)); + --text-light: rgb(var(--light-bg-primary)); + --border-color: rgb(var(--light-border)); + + /* Theme Colors */ + --primary-color: rgb(var(--primary-rgb)); + --primary-color-light: rgba(var(--primary-rgb), 0.1); + --primary-color-dark: rgba(var(--primary-rgb), 0.8); + + --success-color: rgb(var(--success-rgb)); + --success-color-light: rgba(var(--success-rgb), 0.1); + --success-color-dark: rgba(var(--success-rgb), 0.8); + + --warning-color: rgb(var(--warning-rgb)); + --warning-color-light: rgba(var(--warning-rgb), 0.1); + --warning-color-dark: rgba(var(--warning-rgb), 0.8); + + --error-color: rgb(var(--error-rgb)); + --error-color-light: rgba(var(--error-rgb), 0.1); + --error-color-dark: rgba(var(--error-rgb), 0.8); + + --info-color: rgb(var(--info-rgb)); + --info-color-light: rgba(var(--info-rgb), 0.1); + --info-color-dark: rgba(var(--info-rgb), 0.8); + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, rgb(var(--primary-rgb)), rgba(var(--primary-rgb), 0.8)); + --gradient-success: linear-gradient(135deg, rgb(var(--success-rgb)), rgba(var(--success-rgb), 0.8)); + --gradient-warning: linear-gradient(135deg, rgb(var(--warning-rgb)), rgba(var(--warning-rgb), 0.8)); + --gradient-error: linear-gradient(135deg, rgb(var(--error-rgb)), rgba(var(--error-rgb), 0.8)); + --gradient-info: linear-gradient(135deg, rgb(var(--info-rgb)), rgba(var(--info-rgb), 0.8)); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(var(--light-shadow), 0.05); + --shadow-md: 0 4px 6px -1px rgba(var(--light-shadow), 0.1), 0 2px 4px -1px rgba(var(--light-shadow), 0.06); + --shadow-lg: 0 10px 15px -3px rgba(var(--light-shadow), 0.1), 0 4px 6px -2px rgba(var(--light-shadow), 0.05); + --shadow-xl: 0 20px 25px -5px rgba(var(--light-shadow), 0.1), 0 10px 10px -5px rgba(var(--light-shadow), 0.04); + + /* Filters */ + --backdrop-blur: blur(8px); + --backdrop-brightness: brightness(1.1); + --backdrop-saturate: saturate(1.2); +} + +/* Dark Theme */ +[data-theme="dark"] { + /* Colors */ + --bg-primary: rgb(var(--dark-bg-primary)); + --bg-secondary: rgb(var(--dark-bg-secondary)); + --bg-tertiary: rgb(var(--dark-bg-tertiary)); + --text-primary: rgb(var(--dark-text-primary)); + --text-secondary: rgb(var(--dark-text-secondary)); + --text-light: rgb(var(--dark-text-primary)); + --border-color: rgb(var(--dark-border)); + + /* Theme Colors */ + --primary-color: rgb(var(--primary-rgb)); + --primary-color-light: rgba(var(--primary-rgb), 0.2); + --primary-color-dark: rgba(var(--primary-rgb), 0.9); + + --success-color: rgb(var(--success-rgb)); + --success-color-light: rgba(var(--success-rgb), 0.2); + --success-color-dark: rgba(var(--success-rgb), 0.9); + + --warning-color: rgb(var(--warning-rgb)); + --warning-color-light: rgba(var(--warning-rgb), 0.2); + --warning-color-dark: rgba(var(--warning-rgb), 0.9); + + --error-color: rgb(var(--error-rgb)); + --error-color-light: rgba(var(--error-rgb), 0.2); + --error-color-dark: rgba(var(--error-rgb), 0.9); + + --info-color: rgb(var(--info-rgb)); + --info-color-light: rgba(var(--info-rgb), 0.2); + --info-color-dark: rgba(var(--info-rgb), 0.9); + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, rgba(var(--primary-rgb), 0.9), rgba(var(--primary-rgb), 0.7)); + --gradient-success: linear-gradient(135deg, rgba(var(--success-rgb), 0.9), rgba(var(--success-rgb), 0.7)); + --gradient-warning: linear-gradient(135deg, rgba(var(--warning-rgb), 0.9), rgba(var(--warning-rgb), 0.7)); + --gradient-error: linear-gradient(135deg, rgba(var(--error-rgb), 0.9), rgba(var(--error-rgb), 0.7)); + --gradient-info: linear-gradient(135deg, rgba(var(--info-rgb), 0.9), rgba(var(--info-rgb), 0.7)); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(var(--dark-shadow), 0.1); + --shadow-md: 0 4px 6px -1px rgba(var(--dark-shadow), 0.15), 0 2px 4px -1px rgba(var(--dark-shadow), 0.1); + --shadow-lg: 0 10px 15px -3px rgba(var(--dark-shadow), 0.15), 0 4px 6px -2px rgba(var(--dark-shadow), 0.1); + --shadow-xl: 0 20px 25px -5px rgba(var(--dark-shadow), 0.15), 0 10px 10px -5px rgba(var(--dark-shadow), 0.1); + + /* Filters */ + --backdrop-blur: blur(8px); + --backdrop-brightness: brightness(0.9); + --backdrop-saturate: saturate(1.1); +} + +/* Common Variables */ +:root { + /* Typography */ + --font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + --font-size-4xl: 2.25rem; + + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; + + /* Spacing */ + --spacing-0: 0; + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 0.75rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-8: 2rem; + --spacing-10: 2.5rem; + --spacing-12: 3rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + --spacing-32: 8rem; + + /* Border Radius */ + --radius-none: 0; + --radius-sm: 0.125rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-full: 9999px; + + /* Transitions */ + --transition-all: all 0.2s ease-in-out; + --transition-colors: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out; + --transition-opacity: opacity 0.2s ease-in-out; + --transition-shadow: box-shadow 0.2s ease-in-out; + --transition-transform: transform 0.2s ease-in-out; + + /* Z-Index */ + --z-0: 0; + --z-10: 10; + --z-20: 20; + --z-30: 30; + --z-40: 40; + --z-50: 50; + --z-auto: auto; + + /* Focus Ring */ + --focus-ring-width: 2px; + --focus-ring-color: rgba(var(--primary-rgb), 0.6); + --focus-ring-offset: 2px; +} diff --git a/css/utils/animations.css b/css/utils/animations.css new file mode 100644 index 0000000..0240293 --- /dev/null +++ b/css/utils/animations.css @@ -0,0 +1,141 @@ +@keyframes pulse { + 0%, 100% { box-shadow: 0 0 5px var(--error-color); } + 50% { box-shadow: 0 0 20px var(--error-color); } +} + +.neon-text { + font-family: 'Orbitron', sans-serif; + color: var(--text-color); + text-shadow: 0 0 5px var(--text-color), + 0 0 10px var(--text-color), + 0 0 15px var(--accent-color), + 0 0 20px var(--accent-color); + text-align: center; + margin-bottom: 2rem; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Scroll-based animations */ +.animate-on-scroll { + opacity: 0; + transition: opacity 0.6s ease, transform 0.6s ease; +} + +.animate-in { + opacity: 1; + transform: translateY(0) !important; +} + +/* Apply different animations based on position */ +.feature-card:nth-child(odd) { + transform: translateX(-30px); +} + +.feature-card:nth-child(even) { + transform: translateX(30px); +} + +.tools-grid > * { + transform: translateY(30px); +} + +.about-content { + transform: translateY(30px); +} + +.contribute-content { + transform: translateY(30px); +} + +/* Menu transitions */ +.nav-links { + transition: transform 0.3s ease, opacity 0.3s ease; +} + +body.menu-open { + overflow: hidden; +} + +/* Theme transition */ +body { + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Hover animations */ +.feature-card, +.cta-button, +.nav-link, +.theme-toggle button { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.feature-card:hover, +.cta-button:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); +} + +/* Loading animations */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.loading { + background: linear-gradient( + 90deg, + rgba(var(--bg-secondary-rgb), 0.6) 25%, + rgba(var(--bg-secondary-rgb), 0.8) 37%, + rgba(var(--bg-secondary-rgb), 0.6) 63% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} diff --git a/css/utils/utilities.css b/css/utils/utilities.css new file mode 100644 index 0000000..72edba0 --- /dev/null +++ b/css/utils/utilities.css @@ -0,0 +1,137 @@ +/** + * Utility Classes + */ + +/* Display */ +.block { display: block; } +.inline { display: inline; } +.inline-block { display: inline-block; } +.hidden { display: none; } + +/* Visibility */ +.visible { visibility: visible; } +.invisible { visibility: hidden; } + +/* Position */ +.relative { position: relative; } +.absolute { position: absolute; } +.fixed { position: fixed; } +.sticky { position: sticky; } + +/* Text alignment */ +.text-left { text-align: left; } +.text-center { text-align: center; } +.text-right { text-align: right; } +.text-justify { text-align: justify; } + +/* Text styles */ +.text-bold { font-weight: bold; } +.text-normal { font-weight: normal; } +.text-italic { font-style: italic; } +.text-uppercase { text-transform: uppercase; } +.text-capitalize { text-transform: capitalize; } +.text-underline { text-decoration: underline; } +.text-nowrap { white-space: nowrap; } +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Text colors */ +.text-primary { color: var(--primary-color); } +.text-secondary { color: var(--secondary-color); } +.text-success { color: var(--success-color); } +.text-warning { color: var(--warning-color); } +.text-error { color: var(--error-color); } +.text-muted { color: var(--text-muted); } + +/* Background colors */ +.bg-primary { background-color: var(--primary-color); } +.bg-secondary { background-color: var(--secondary-color); } +.bg-success { background-color: var(--success-color); } +.bg-warning { background-color: var(--warning-color); } +.bg-error { background-color: var(--error-color); } +.bg-dark { background-color: var(--bg-dark); } +.bg-light { background-color: var(--bg-light); } + +/* Padding utilities */ +.p-0 { padding: 0; } +.p-1 { padding: var(--spacing-xs); } +.p-2 { padding: var(--spacing-sm); } +.p-3 { padding: var(--spacing-md); } +.p-4 { padding: var(--spacing-lg); } +.p-5 { padding: var(--spacing-xl); } + +.px-1 { padding-left: var(--spacing-xs); padding-right: var(--spacing-xs); } +.px-2 { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); } +.px-3 { padding-left: var(--spacing-md); padding-right: var(--spacing-md); } +.px-4 { padding-left: var(--spacing-lg); padding-right: var(--spacing-lg); } +.px-5 { padding-left: var(--spacing-xl); padding-right: var(--spacing-xl); } + +.py-1 { padding-top: var(--spacing-xs); padding-bottom: var(--spacing-xs); } +.py-2 { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); } +.py-3 { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); } +.py-4 { padding-top: var(--spacing-lg); padding-bottom: var(--spacing-lg); } +.py-5 { padding-top: var(--spacing-xl); padding-bottom: var(--spacing-xl); } + +/* Border radius */ +.rounded-none { border-radius: 0; } +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-full { border-radius: var(--radius-full); } + +/* Shadows */ +.shadow-none { box-shadow: none; } +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } + +/* Opacity */ +.opacity-0 { opacity: 0; } +.opacity-25 { opacity: 0.25; } +.opacity-50 { opacity: 0.5; } +.opacity-75 { opacity: 0.75; } +.opacity-100 { opacity: 1; } + +/* Cursor */ +.cursor-pointer { cursor: pointer; } +.cursor-not-allowed { cursor: not-allowed; } +.cursor-wait { cursor: wait; } +.cursor-text { cursor: text; } + +/* Object fit */ +.object-contain { object-fit: contain; } +.object-cover { object-fit: cover; } +.object-fill { object-fit: fill; } +.object-none { object-fit: none; } + +/* Z-index */ +.z-negative { z-index: var(--z-negative); } +.z-normal { z-index: var(--z-normal); } +.z-tooltip { z-index: var(--z-tooltip); } +.z-fixed { z-index: var(--z-fixed); } +.z-modal { z-index: var(--z-modal); } + +/* Overflow */ +.overflow-auto { overflow: auto; } +.overflow-hidden { overflow: hidden; } +.overflow-scroll { overflow: scroll; } +.overflow-x-auto { overflow-x: auto; } +.overflow-y-auto { overflow-y: auto; } + +/* Width and Height */ +.w-full { width: 100%; } +.w-screen { width: 100vw; } +.h-full { height: 100%; } +.h-screen { height: 100vh; } + +/* Responsive hide/show */ +@media (max-width: 768px) { + .hide-mobile { display: none; } +} + +@media (min-width: 769px) { + .hide-desktop { display: none; } +} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..c8eb93d --- /dev/null +++ b/docs/API.md @@ -0,0 +1,251 @@ +# API Documentation + +## Core APIs + +### ToolsManager + +The `ToolsManager` class provides methods for managing and interacting with tools. + +```typescript +interface Tool { + id: string; + name: string; + description: string; + icon: string; + features: string[]; + path: string; + category: string; + order: number; +} + +class ToolsManager { + static getAllTools(): Tool[]; + static getToolsByCategory(category: string): Tool[]; + static getToolById(id: string): Tool | undefined; + static getCategoriesWithInfo(): Record; + static getCategoryInfo(category: string): CategoryInfo | undefined; + static getStats(): ToolStats; + static isValidCategory(category: string): boolean; + static getToolsSortedByOrder(): Tool[]; + static searchTools(query: string): Tool[]; +} +``` + +### Storage Utilities + +The storage utilities provide a consistent interface for data persistence. + +```typescript +interface StorageOptions { + type?: 'local' | 'session'; +} + +function isStorageAvailable(type: string): boolean; +function getStorageItem(key: string, type?: 'local' | 'session'): T | null; +function setStorageItem(key: string, value: any, type?: 'local' | 'session'): boolean; +function removeStorageItem(key: string, type?: 'local' | 'session'): boolean; +function clearStorage(type?: 'local' | 'session'): boolean; +``` + +### DOM Utilities + +Utilities for DOM manipulation and browser interactions. + +```typescript +function sanitizeHTML(html: string): string; +function isInViewport(element: Element, offset?: number): boolean; +function copyToClipboard(text: string): Promise; +function isMobile(): boolean; +function getBrowserLanguage(): string; +``` + +### Format Utilities + +Utilities for data formatting and conversion. + +```typescript +interface DateFormatOptions extends Intl.DateTimeFormatOptions {} + +function formatDate(date: Date | string | number, options?: DateFormatOptions): string; +function formatRelativeTime(date: Date | string | number): string; +function formatFileSize(bytes: number): string; +function getFileExtension(filename: string): string; +function rgbToHex(r: number, g: number, b: number): string; +function hexToRgb(hex: string): { r: number; g: number; b: number } | null; +function isLightColor(color: string): boolean; +``` + +## Component APIs + +### Card Component + +The card component provides a flexible container for content. + +```typescript +interface CardProps { + variant?: 'default' | 'hover' | 'interactive' | 'gradient'; + size?: 'sm' | 'md' | 'lg'; + theme?: 'primary' | 'secondary'; + className?: string; +} + +// Usage example: +
+
...
+
...
+ +
+``` + +### Alert Component + +The alert component provides feedback messages. + +```typescript +interface AlertProps { + type?: 'info' | 'success' | 'warning' | 'error'; + size?: 'sm' | 'md' | 'lg'; + dismissible?: boolean; + animate?: boolean; +} + +// Usage example: +
+
...
+
+
...
+
...
+
+ +
+``` + +## Configuration + +### Theme Configuration + +Theme variables and customization options. + +```typescript +interface ThemeConfig { + colors: { + primary: string; + secondary: string; + success: string; + warning: string; + error: string; + }; + spacing: { + xs: string; + sm: string; + md: string; + lg: string; + xl: string; + }; + radius: { + sm: string; + md: string; + lg: string; + full: string; + }; +} + +// Usage example: +const theme: ThemeConfig = { + colors: { + primary: '#00ffff', + secondary: '#ff00de', + // ... + }, + // ... +}; +``` + +### Tool Configuration + +Configuration options for tools. + +```typescript +interface ToolConfig { + id: string; + name: string; + description: string; + icon: string; + features: string[]; + path: string; + category: keyof typeof TOOL_CATEGORIES; + order: number; +} + +// Usage example: +const toolConfig: ToolConfig = { + id: 'text-to-speech', + name: 'Text to Speech', + description: 'Convert text to natural-sounding speech', + icon: 'fa-volume-up', + features: ['Multiple voices', 'Download audio'], + path: './pages/text-to-speech.html', + category: TOOL_CATEGORIES.AUDIO, + order: 1 +}; +``` + +## Events + +### Custom Events + +The application uses custom events for communication. + +```typescript +interface ToolEvent extends CustomEvent { + detail: { + toolId: string; + action: 'load' | 'unload' | 'update'; + data?: any; + }; +} + +// Dispatch event +window.dispatchEvent(new CustomEvent('tool:load', { + detail: { + toolId: 'text-to-speech', + action: 'load' + } +})); + +// Listen for event +window.addEventListener('tool:load', (event: ToolEvent) => { + const { toolId, action } = event.detail; + // Handle event +}); +``` + +## Error Handling + +Standard error types and handling patterns. + +```typescript +interface ToolError extends Error { + code: string; + toolId?: string; + data?: any; +} + +class ValidationError extends Error { + constructor(message: string, public code: string) { + super(message); + this.name = 'ValidationError'; + } +} + +// Usage example: +try { + // Tool operation +} catch (error) { + if (error instanceof ValidationError) { + // Handle validation error + } else { + // Handle other errors + } +} +``` \ No newline at end of file diff --git a/docs/FUTURE-FEATURES.md b/docs/FUTURE-FEATURES.md new file mode 100644 index 0000000..3599c43 --- /dev/null +++ b/docs/FUTURE-FEATURES.md @@ -0,0 +1,249 @@ +# Future Features and Enhancements + +## 🔄 Planned Updates for Existing Tools + +### Text to Speech +- [ ] Additional language support +- [ ] Voice customization options +- [ ] Background music mixing +- [ ] Batch processing for multiple texts +- [ ] Export in multiple audio formats +- [ ] Voice emotion control +- [ ] Text formatting preservation +- [ ] Real-time voice preview + +### Image Resizer +- [ ] Batch processing +- [ ] Advanced cropping tools +- [ ] Image filters and effects +- [ ] Compression optimization +- [ ] Custom presets +- [ ] Metadata preservation +- [ ] Format conversion +- [ ] Background removal + +### Color Palette Generator +- [ ] AI-powered palette suggestions +- [ ] Image color extraction +- [ ] Accessibility contrast checker +- [ ] Brand color guidelines export +- [ ] Color blindness simulation +- [ ] CSS gradient generator +- [ ] Pattern preview +- [ ] Design system export + +### ASCII Art Generator +- [ ] Video to ASCII conversion +- [ ] Custom character sets +- [ ] Animation support +- [ ] Color preservation +- [ ] Style presets +- [ ] Real-time preview +- [ ] Multiple output formats +- [ ] Batch processing + +### QR Code Generator +- [ ] Custom design templates +- [ ] Logo integration +- [ ] Animated QR codes +- [ ] Batch generation +- [ ] Advanced styling options +- [ ] Error correction level control +- [ ] SVG output +- [ ] Analytics integration + +### Password Generator +- [ ] Password strength analyzer +- [ ] Custom character sets +- [ ] Memorable password options +- [ ] Password history +- [ ] Secure storage +- [ ] Password policy templates +- [ ] Batch generation +- [ ] Export options + +### URL Shortener +- [ ] Custom domain support +- [ ] Analytics dashboard +- [ ] QR code integration +- [ ] Link expiration +- [ ] Password protection +- [ ] Click tracking +- [ ] API access +- [ ] Bulk shortening + +## 🆕 New Tools in Development + +### File Converter +- Multiple format support +- Batch processing +- Custom conversion settings +- Preview functionality +- Cloud storage integration + +### PDF Tools +- Merge PDFs +- Split PDFs +- Convert to/from PDF +- Add watermarks +- Extract images +- Compress PDFs +- Form filling +- Digital signatures + +### Image Editor +- Basic editing tools +- Filters and effects +- Text overlay +- Shape tools +- Layer support +- Export options +- Template system +- Mobile support + +### Code Formatter +- Multiple language support +- Custom formatting rules +- Minification +- Beautification +- Syntax highlighting +- Error detection +- Code sharing +- Plugin system + +### Markdown Editor +- Live preview +- Custom themes +- Export options +- Table generator +- Image handling +- Version history +- Collaboration +- Templates + +### SVG Editor +- Basic shape tools +- Path editing +- Animation support +- Export options +- Optimization +- Icon generator +- Template library +- Responsive preview + +### Data Converter +- JSON/XML/CSV conversion +- Data validation +- Schema support +- Preview mode +- Custom formatting +- Batch processing +- API integration +- Export options + +## 🚀 Platform Enhancements + +### Performance +- [ ] Progressive Web App (PWA) implementation +- [ ] Service worker for offline support +- [ ] Resource optimization +- [ ] Lazy loading improvements +- [ ] Caching strategies +- [ ] Performance monitoring +- [ ] CDN integration +- [ ] Build optimization + +### User Experience +- [ ] Customizable workspace +- [ ] Tool combinations +- [ ] Keyboard shortcuts +- [ ] Touch gestures +- [ ] Context menus +- [ ] Quick actions +- [ ] Tutorial system +- [ ] User preferences + +### Accessibility +- [ ] Screen reader optimization +- [ ] Keyboard navigation +- [ ] High contrast themes +- [ ] Font size controls +- [ ] Motion reduction +- [ ] Color blind modes +- [ ] Voice commands +- [ ] Accessibility checker + +### Integration +- [ ] Cloud storage support +- [ ] Social sharing +- [ ] API endpoints +- [ ] Webhook support +- [ ] Export plugins +- [ ] Third-party integrations +- [ ] Browser extensions +- [ ] Mobile apps + +### Security +- [ ] End-to-end encryption +- [ ] Two-factor authentication +- [ ] Privacy controls +- [ ] Data backup +- [ ] Access logging +- [ ] Security auditing +- [ ] Compliance checks +- [ ] Vulnerability scanning + +### Analytics +- [ ] Usage tracking +- [ ] Performance metrics +- [ ] Error reporting +- [ ] User feedback +- [ ] A/B testing +- [ ] Feature analytics +- [ ] Conversion tracking +- [ ] Heat maps + +## 📅 Timeline + +### Q1 2024 +- PWA implementation +- Performance optimization +- New tool: File Converter +- Existing tool enhancements + +### Q2 2024 +- Mobile responsiveness +- Cloud integration +- New tool: PDF Tools +- Security improvements + +### Q3 2024 +- API development +- Analytics implementation +- New tool: Image Editor +- Platform stability + +### Q4 2024 +- User accounts +- Collaboration features +- New tools: Code Formatter & Markdown Editor +- Performance monitoring + +## 🤝 Contributing + +We welcome contributions! If you'd like to help implement any of these features: + +1. Check the [CONTRIBUTING.md](CONTRIBUTING.md) guide +2. Join our community discussions +3. Pick a feature to work on +4. Submit a pull request + +## 📢 Feedback + +Have a feature request? Let us know: +- Create an issue +- Join our discussions +- Send us feedback +- Vote on features + +Stay tuned for updates and new feature announcements! diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ec8c531 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,187 @@ +# Digital Services Hub + +A comprehensive suite of free, web-based tools that simplify everyday digital tasks. + +## 🎯 Vision + +Digital Services Hub provides a modern, accessible platform focused on: +- 🌟 User-friendly and intuitive interfaces +- 🔒 Privacy-first approach with client-side processing +- ♿ Accessibility and inclusive design +- 🚀 Performance and efficiency +- 💻 Cross-platform compatibility +- 🌐 Global availability + +## 🛠️ Available Tools + +### Audio Tools +- **Text to Speech**: Convert text to natural-sounding speech with multiple voices and languages + +### Image Tools +- **Image Resizer**: Resize and optimize images while maintaining quality +- **ASCII Art**: Convert images into creative ASCII art +- **Color Palette**: Generate beautiful color harmonies for designs + +### Utility Tools +- **QR Code Generator**: Create customizable QR codes +- **Password Generator**: Generate secure passwords with advanced options +- **URL Shortener**: Create short, trackable links + +## 🚀 UI Components + +The project includes a comprehensive set of accessible, themeable UI components: + +### Core Components +- **Buttons**: Multiple variants, sizes, and states with keyboard navigation +- **Forms**: Fully accessible form controls with validation states +- **Alerts**: Customizable notifications with auto-dismiss +- **Progress**: Interactive progress bars with various states +- **Modal**: Accessible dialog system with keyboard trapping +- **Tooltip**: Customizable tooltips with multiple positions +- **Navigation**: Responsive navigation with mobile support + +### Features +- **Dark/Light Theme**: Automatic theme detection with manual override +- **Responsive Design**: Mobile-first approach with fluid layouts +- **Accessibility**: ARIA attributes and keyboard navigation +- **Animations**: Smooth transitions and loading states +- **Error Handling**: Comprehensive error states and feedback + +## 🚀 Getting Started + +1. Clone the repository: + ```bash + git clone https://github.com/TMHDigital/Digital_Services.HUB.git + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Start the development server: + ```bash + npm run dev + ``` + +## 🏗️ Project Structure + +``` +Digital_Services.HUB/ +├── css/ # Stylesheets +│ ├── base/ # Base styles and reset +│ ├── components/ # Component styles +│ │ ├── alerts.css +│ │ ├── ascii-art.css +│ │ ├── buttons.css +│ │ ├── cards.css +│ │ ├── color-palette.css +│ │ ├── forms.css +│ │ ├── image-resizer.css +│ │ ├── modal.css +│ │ ├── navigation.css +│ │ ├── notifications.css +│ │ ├── password-generator.css +│ │ ├── progress.css +│ │ ├── qr-code.css +│ │ ├── social-share.css +│ │ ├── spinner.css +│ │ ├── text-to-speech.css +│ │ ├── tool-page.css +│ │ ├── tooltip.css +│ │ ├── ui.css +│ │ └── url-shortener.css +│ ├── themes/ # Theme configuration +│ ├── utils/ # Utility styles +│ └── styles.css # Main stylesheet +├── js/ # JavaScript files +│ ├── api/ # API integrations +│ │ └── text-to-speech-api.js +│ ├── build/ # Build scripts +│ ├── components/ # UI components +│ ├── config/ # Configuration +│ ├── features/ # Tool implementations +│ │ ├── about.js +│ │ ├── ascii-art.js +│ │ ├── base-tool.js +│ │ ├── color-palette.js +│ │ ├── image-resizer.js +│ │ ├── password-generator.js +│ │ ├── qr-code.js +│ │ ├── text-to-speech.js +│ │ ├── tools-manager.js +│ │ └── url-shortener.js +│ ├── templates/ # HTML templates +│ └── utils/ # Utility functions +├── pages/ # Tool pages +│ ├── ascii-art.html +│ ├── color-palette.html +│ ├── image-resizer.html +│ ├── password-generator.html +│ ├── qr-code.html +│ ├── text-to-speech.html +│ └── url-shortener.html +├── docs/ # Documentation +│ ├── API.md # API documentation +│ ├── FUTURE-FEATURES.md +│ ├── README.md # Main documentation +│ └── TECHNICAL.md # Technical documentation +├── index.html # Main entry point +├── package.json # Project dependencies +├── rollup.config.js # Build configuration +├── .eslintrc.json # ESLint configuration +├── .gitignore # Git ignore rules +├── CHANGELOG.md # Version history +├── CONTRIBUTING.md # Contribution guidelines +├── LICENSE # Project license +└── TO-DO.md # Project tasks +``` + +## 🛠️ Development + +### Prerequisites +- Node.js 16+ +- npm or yarn +- Modern web browser + +### Commands +- `npm run dev`: Start development server +- `npm run build`: Build for production +- `npm run test`: Run tests +- `npm run lint`: Lint code +- `npm run validate`: Validate project structure + +### Component Development +1. Follow the established component structure in `js/components/` +2. Ensure accessibility features are implemented +3. Add corresponding styles in `css/components/` +4. Include dark theme support +5. Add tests in `js/components/__tests__/` + +### Contributing +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## 📝 Documentation + +- [Technical Documentation](./TECHNICAL.md) +- [API Documentation](./API.md) +- [Contributing Guidelines](../CONTRIBUTING.md) +- [Code of Conduct](../CODE_OF_CONDUCT.md) + +## 🗺️ Roadmap + +See our [Future Features](./FUTURE-FEATURES.md) document for planned improvements and upcoming tools. + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details. + +## 🤝 Support + +- Report bugs: [Issue Tracker](https://github.com/TMHDigital/Digital_Services.HUB/issues) +- Request features: [Feature Requests](https://github.com/TMHDigital/Digital_Services.HUB/issues/new) +- Join discussions: [Discussions](https://github.com/TMHDigital/Digital_Services.HUB/discussions) diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md new file mode 100644 index 0000000..a6d6abb --- /dev/null +++ b/docs/TECHNICAL.md @@ -0,0 +1,309 @@ +# Technical Documentation + +## UI Components + +### Button Component +```javascript +import { Button } from '../components/button.js'; + +// Basic usage +const button = new Button({ + text: 'Click me', + variant: 'primary', + size: 'md', + onClick: () => console.log('Clicked!') +}); + +// Available variants +- primary +- secondary +- success +- warning +- error + +// Available sizes +- sm (small) +- md (medium) +- lg (large) +``` + +### Alert Component +```javascript +import { Alert } from '../components/alert.js'; + +// Show alert +Alert.show('Operation successful!', { + type: 'success', + duration: 3000, + dismissible: true +}); + +// Available types +- info +- success +- warning +- error +``` + +### Modal Component +```javascript +import { Modal } from '../components/modal.js'; + +const modal = new Modal({ + title: 'Confirmation', + content: 'Are you sure?', + closable: true, + size: 'md', + onOpen: () => console.log('Modal opened'), + onClose: () => console.log('Modal closed') +}); + +modal.open(); +``` + +### Tooltip Component +```javascript +import { Tooltip } from '../components/tooltip.js'; + +const tooltip = new Tooltip(element, 'Helpful text', { + position: 'top', + theme: 'dark', + showDelay: 200, + hideDelay: 200 +}); +``` + +### Progress Component +```css +/* Basic progress bar */ +
+
+
+ +/* With label */ +
+
+ Progress + 50% +
+
+
+
+
+``` + +### Form Components +```html + +
+ + +
+ + +
+ + +
+ + + +``` + +## Theme System + +### Theme Variables +```css +:root { + /* Colors */ + --primary-color: #3b82f6; + --success-color: #22c55e; + --warning-color: #f59e0b; + --error-color: #ef4444; + + /* Typography */ + --font-family: 'Roboto', sans-serif; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + + /* Spacing */ + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 0.75rem; + --spacing-4: 1rem; +} +``` + +### Theme Switching +```javascript +import { setTheme, getTheme } from '../utils/theme.js'; + +// Switch theme +setTheme('dark'); + +// Get current theme +const currentTheme = getTheme(); // 'light' | 'dark' | 'system' + +// Listen for theme changes +addThemeObserver((theme) => { + console.log(`Theme changed to: ${theme}`); +}); +``` + +## Utility Functions + +### DOM Utilities +```javascript +import { createElement, addEventListeners } from '../utils/dom.js'; + +// Create element with attributes and children +const button = createElement('button', { + className: 'btn btn-primary', + onClick: () => console.log('Clicked!') +}, ['Click me']); + +// Add multiple event listeners +addEventListeners(element, { + click: () => console.log('Clicked'), + mouseenter: () => console.log('Mouse entered'), + mouseleave: () => console.log('Mouse left') +}); +``` + +### Storage Utilities +```javascript +import { setStorageItem, getStorageItem } from '../utils/storage.js'; + +// Save data +setStorageItem('user-preferences', { theme: 'dark' }); + +// Get data +const preferences = getStorageItem('user-preferences'); +``` + +### Format Utilities +```javascript +import { formatFileSize, formatDate } from '../utils/format.js'; + +// Format file size +const size = formatFileSize(1024); // '1 KB' + +// Format date +const date = formatDate(new Date(), { + dateStyle: 'full', + timeStyle: 'short' +}); +``` + +## Base Tool Class + +All tools extend the `BaseTool` class which provides common functionality: + +```javascript +import { BaseTool } from './base-tool.js'; + +class MyTool extends BaseTool { + constructor() { + super(); + this.elements = this.initializeElements(); + this.state = this.initializeState(); + this.initialize(); + this.bindEvents(); + } + + // Required methods + initializeElements() { + return { + // DOM elements + }; + } + + initializeState() { + return { + // Initial state + }; + } + + bindEvents() { + // Bind event listeners + } + + initialize() { + // Initialize tool + } +} +``` + +## Build System + +### Commands +```bash +# Development +npm run dev # Start development server +npm run build # Build for production +npm run test # Run tests +npm run lint # Lint code +npm run validate # Validate project structure +``` + +### Build Configuration +```javascript +// rollup.config.js +export default { + input: 'js/utils/app.js', + output: { + file: 'dist/bundle.js', + format: 'es', + sourcemap: true + }, + plugins: [ + nodeResolve(), + terser({ + format: { + comments: false + } + }) + ] +}; +``` + +## Testing + +### Component Testing +```javascript +import { Modal } from '../components/modal.js'; + +describe('Modal', () => { + test('opens and closes correctly', () => { + const modal = new Modal({ + title: 'Test', + content: 'Content' + }); + + modal.open(); + expect(modal.isOpen).toBe(true); + + modal.close(); + expect(modal.isOpen).toBe(false); + }); +}); +``` + +### Tool Testing +```javascript +import { TextToSpeech } from '../features/text-to-speech.js'; + +describe('TextToSpeech', () => { + test('initializes correctly', () => { + const tts = new TextToSpeech(); + expect(tts.isInitialized).toBe(true); + expect(tts.elements.textInput).toBeDefined(); + }); +}); diff --git a/images/.png b/images/.png deleted file mode 100644 index 8b13789..0000000 --- a/images/.png +++ /dev/null @@ -1 +0,0 @@ - diff --git a/index.html b/index.html index 87c0df2..5e55a28 100644 --- a/index.html +++ b/index.html @@ -3,28 +3,135 @@ - Digital Services Hub + Digital Services Hub - Free Web Tools for Digital Tasks + + + + + + + + + + + + + + + + + + + + + + + -
-

Digital Services Hub

- - - -
-

Welcome to our Digital Services Hub!

-

We offer a range of free tools to help with your digital media needs:

- - - -

Choose a service above to get started, or visit our About page to learn more.

+ + + +
+
+

Digital Services Hub

+

Powerful Web Tools at Your Fingertips

+
-
+
+
+ Fast & Free +
+
+ Secure +
+
+ Accessible +
+
+ + +
+ +
+

Our Tools

+

Everything you need, all in one place

+
+ +
+
+ + +
+

Why Choose Us

+
+
+ +

Fast & Efficient

+

Lightning-fast tools optimized for performance, with no delays or waiting.

+
+
+ +

100% Secure

+

Your data stays in your browser. No uploads, no tracking, complete privacy.

+
+
+ +

Accessible

+

Built with accessibility in mind, ensuring everyone can use our tools.

+
+
+
+ + +
+

About Us

+
+

Digital Services Hub is dedicated to providing free, accessible, and powerful web-based tools for everyday digital tasks. We believe that quality digital tools should be available to everyone, regardless of technical expertise or budget.

+
+
+ 10+ + Tools +
+
+ 100% + Free +
+
+ 24/7 + Available +
+
+
+
+ + +
+
+

Contribute

+

Join our open-source community and help make digital tools accessible to everyone.

+ +
+
+
+ + - + + + diff --git a/js/color-palette.js b/js/color-palette.js deleted file mode 100644 index 33dab09..0000000 --- a/js/color-palette.js +++ /dev/null @@ -1,47 +0,0 @@ -const fileInput = document.getElementById('file-input'); -const imageContainer = document.getElementById('image-container'); -const paletteContainer = document.getElementById('palette-container'); -const generateButton = document.getElementById('generate-button'); - -fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - imageContainer.innerHTML = ``; - }; - reader.readAsDataURL(file); - } -}); - -generateButton.addEventListener('click', () => { - const image = document.getElementById('image'); - if (image) { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = image.width; - canvas.height = image.height; - ctx.drawImage(image, 0, 0, image.width, image.height); - - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - const colorMap = {}; - - for (let i = 0; i < imageData.length; i += 4) { - const r = imageData[i]; - const g = imageData[i + 1]; - const b = imageData[i + 2]; - const rgb = `rgb(${r},${g},${b})`; - colorMap[rgb] = (colorMap[rgb] || 0) + 1; - } - - const sortedColors = Object.entries(colorMap).sort((a, b) => b[1] - a[1]); - const topColors = sortedColors.slice(0, 5); - - paletteContainer.innerHTML = topColors.map(([color, count]) => ` -
-
-
${color}
-
- `).join(''); - } -}); diff --git a/js/common.js b/js/common.js deleted file mode 100644 index f1a4006..0000000 --- a/js/common.js +++ /dev/null @@ -1,17 +0,0 @@ -document.addEventListener('DOMContentLoaded', (event) => { - const nav = document.getElementById('main-nav'); - nav.innerHTML = ` - Home - Image Resizer - Color Palette - About - `; - - // Adjust links if we're on the homepage - if (window.location.pathname.endsWith('index.html') || window.location.pathname.endsWith('/')) { - nav.innerHTML = nav.innerHTML.replace('../index.html', 'index.html'); - nav.innerHTML = nav.innerHTML.replace('image-resizer.html', 'pages/image-resizer.html'); - nav.innerHTML = nav.innerHTML.replace('color-palette.html', 'pages/color-palette.html'); - nav.innerHTML = nav.innerHTML.replace('about.html', 'pages/about.html'); - } -}); diff --git a/js/components/layout.js b/js/components/layout.js new file mode 100644 index 0000000..e582e63 --- /dev/null +++ b/js/components/layout.js @@ -0,0 +1,77 @@ +/** + * Layout Injection System + * Dynamically injects the header and footer to ensure consistency across pages. + */ + +export function injectLayout() { + const isSubPage = window.location.pathname.includes('/pages/'); + const basePath = isSubPage ? '../' : './'; + const homeLink = isSubPage ? '../index.html' : './index.html'; // Ensure we go to index.html explicitly + + // If we are on the home page, the anchors are just #id + // If we are on a sub page, the anchors are ../index.html#id + const getLink = (anchor) => isSubPage ? `../index.html${anchor}` : anchor; + + const headerHTML = ` + + `; + + const footerHTML = ` + + `; + + // Inject Header at the start of body + document.body.insertAdjacentHTML('afterbegin', headerHTML); + + // Inject Footer at the end of body + document.body.insertAdjacentHTML('beforeend', footerHTML); +} + diff --git a/js/config/tools.js b/js/config/tools.js new file mode 100644 index 0000000..66d7bfd --- /dev/null +++ b/js/config/tools.js @@ -0,0 +1,192 @@ +/** + * @typedef {Object} Tool + * @property {string} id - Unique identifier for the tool + * @property {string} name - Display name of the tool + * @property {string} description - Tool description + * @property {string} icon - FontAwesome icon class + * @property {string[]} features - List of key features + * @property {string} path - Path to the tool's page + * @property {string} category - Tool category + * @property {number} order - Display order + */ + +/** + * Tool categories + * @readonly + * @enum {string} + */ +export const TOOL_CATEGORIES = { + AUDIO: 'audio', + IMAGE: 'image', + TEXT: 'text', + UTILITY: 'utility', + DESIGN: 'design', + SECURITY: 'security' +}; + +/** @type {Tool[]} */ +export const TOOLS = [ + { + id: 'text-to-speech', + name: 'Text to Speech', + description: 'Convert text to natural-sounding speech with multiple voices and languages.', + icon: 'fas fa-volume-up', + features: [ + { name: 'Multiple Voices', icon: 'fas fa-microphone', description: 'Choose from various voices' }, + { name: 'Language Support', icon: 'fas fa-language', description: 'Multiple language options' }, + { name: 'Download Audio', icon: 'fas fa-download', description: 'Save as MP3' } + ], + path: 'pages/text-to-speech.html', + category: TOOL_CATEGORIES.AUDIO, + order: 1 + }, + { + id: 'image-resizer', + name: 'Image Resizer', + description: 'Resize and optimize your images while maintaining quality.', + icon: 'fas fa-image', + features: [ + { name: 'Preserve Ratio', icon: 'fas fa-expand', description: 'Maintain aspect ratio' }, + { name: 'Multiple Formats', icon: 'fas fa-file-image', description: 'Support for PNG, JPG, WebP' }, + { name: 'Batch Processing', icon: 'fas fa-layer-group', description: 'Process multiple images' } + ], + path: 'pages/image-resizer.html', + category: TOOL_CATEGORIES.IMAGE, + order: 2 + }, + { + id: 'ascii-art', + name: 'ASCII Art', + description: 'Convert images into creative ASCII art.', + icon: 'fas fa-font', + features: [ + { name: 'Custom Characters', icon: 'fas fa-keyboard', description: 'Choose character set' }, + { name: 'Size Control', icon: 'fas fa-arrows-alt', description: 'Adjust output size' }, + { name: 'Export Options', icon: 'fas fa-file-export', description: 'Save as text or image' } + ], + path: 'pages/ascii-art.html', + category: TOOL_CATEGORIES.IMAGE, + order: 3 + }, + { + id: 'color-palette', + name: 'Color Palette', + description: 'Generate beautiful color harmonies for designs.', + icon: 'fas fa-palette', + features: [ + { name: 'Color Harmonies', icon: 'fas fa-sync', description: 'Generate matching colors' }, + { name: 'Export Formats', icon: 'fas fa-file-code', description: 'CSS, SCSS, JSON' }, + { name: 'Accessibility', icon: 'fas fa-universal-access', description: 'Check contrast ratios' } + ], + path: 'pages/color-palette.html', + category: TOOL_CATEGORIES.DESIGN, + order: 4 + }, + { + id: 'qr-code', + name: 'QR Code Generator', + description: 'Create customizable QR codes for your links and data.', + icon: 'fas fa-qrcode', + features: [ + { name: 'Custom Styles', icon: 'fas fa-paint-brush', description: 'Customize colors and style' }, + { name: 'Multiple Formats', icon: 'fas fa-file-image', description: 'PNG, SVG, PDF' }, + { name: 'Error Correction', icon: 'fas fa-shield-alt', description: 'Reliable scanning' } + ], + path: 'pages/qr-code.html', + category: TOOL_CATEGORIES.UTILITY, + order: 5 + }, + { + id: 'password-generator', + name: 'Password Generator', + description: 'Generate secure passwords with advanced options.', + icon: 'fas fa-key', + features: [ + { name: 'Custom Rules', icon: 'fas fa-sliders-h', description: 'Customize complexity' }, + { name: 'Strength Check', icon: 'fas fa-shield-alt', description: 'Password strength meter' }, + { name: 'Save History', icon: 'fas fa-history', description: 'Recent passwords' } + ], + path: 'pages/password-generator.html', + category: TOOL_CATEGORIES.SECURITY, + order: 6 + }, + { + id: 'url-shortener', + name: 'URL Shortener', + description: 'Create short, trackable links instantly.', + icon: 'fas fa-link', + features: [ + { name: 'Custom Aliases', icon: 'fas fa-tag', description: 'Create memorable links' }, + { name: 'Click Tracking', icon: 'fas fa-chart-line', description: 'Track link usage' }, + { name: 'QR Code Export', icon: 'fas fa-qrcode', description: 'Generate QR codes' } + ], + path: 'pages/url-shortener.html', + category: TOOL_CATEGORIES.UTILITY, + order: 7 + } +]; + +/** + * @typedef {Object} CategoryInfo + * @property {string} name - Display name of the category + * @property {string} description - Category description + * @property {string} icon - FontAwesome icon class + * @property {string} color - Category color + */ + +/** @type {Record} */ +export const CATEGORIES = { + [TOOL_CATEGORIES.AUDIO]: { + name: 'Audio Tools', + description: 'Tools for audio processing and conversion', + icon: 'fas fa-music', + color: '#00f2fe' + }, + [TOOL_CATEGORIES.IMAGE]: { + name: 'Image Tools', + description: 'Tools for image manipulation and conversion', + icon: 'fas fa-image', + color: '#4facfe' + }, + [TOOL_CATEGORIES.DESIGN]: { + name: 'Design Tools', + description: 'Tools for design and color management', + icon: 'fas fa-palette', + color: '#b721ff' + }, + [TOOL_CATEGORIES.UTILITY]: { + name: 'Utility Tools', + description: 'General purpose utility tools', + icon: 'fas fa-tools', + color: '#21d4fd' + }, + [TOOL_CATEGORIES.SECURITY]: { + name: 'Security Tools', + description: 'Tools for security and privacy', + icon: 'fas fa-shield-alt', + color: '#0061ff' + }, + [TOOL_CATEGORIES.TEXT]: { + name: 'Text Tools', + description: 'Tools for text manipulation and processing', + icon: 'fas fa-font', + color: '#60efff' + } +}; + +/** + * @typedef {Object} ToolStats + * @property {number} totalTools - Total number of tools + * @property {Record} toolsByCategory - Number of tools in each category + */ + +/** @type {ToolStats} */ +export const TOOL_STATS = { + totalTools: TOOLS.length, + toolsByCategory: Object.fromEntries( + Object.keys(CATEGORIES).map(category => [ + category, + TOOLS.filter(tool => tool.category === category).length + ]) + ) +}; diff --git a/js/features/__tests__/tools-manager.test.js b/js/features/__tests__/tools-manager.test.js new file mode 100644 index 0000000..2df29a4 --- /dev/null +++ b/js/features/__tests__/tools-manager.test.js @@ -0,0 +1,53 @@ +import { ToolsManager } from '../tools-manager.js'; +import { TOOL_CATEGORIES } from '../../config/tools.js'; + +describe('ToolsManager', () => { + test('getAllTools returns all tools', () => { + const tools = ToolsManager.getAllTools(); + expect(Array.isArray(tools)).toBe(true); + expect(tools.length).toBeGreaterThan(0); + }); + + test('getToolsByCategory returns correct tools', () => { + const imageTools = ToolsManager.getToolsByCategory(TOOL_CATEGORIES.IMAGE); + expect(Array.isArray(imageTools)).toBe(true); + imageTools.forEach(tool => { + expect(tool.category).toBe(TOOL_CATEGORIES.IMAGE); + }); + }); + + test('getToolById returns correct tool', () => { + const tool = ToolsManager.getToolById('image-resizer'); + expect(tool).toBeDefined(); + expect(tool.id).toBe('image-resizer'); + }); + + test('getCategoryInfo returns correct info', () => { + const info = ToolsManager.getCategoryInfo(TOOL_CATEGORIES.IMAGE); + expect(info).toBeDefined(); + expect(info.name).toBe('Image Tools'); + }); + + test('isValidCategory validates categories correctly', () => { + expect(ToolsManager.isValidCategory(TOOL_CATEGORIES.IMAGE)).toBe(true); + expect(ToolsManager.isValidCategory('invalid-category')).toBe(false); + }); + + test('getToolsSortedByOrder returns sorted tools', () => { + const tools = ToolsManager.getToolsSortedByOrder(); + for (let i = 1; i < tools.length; i++) { + expect(tools[i].order).toBeGreaterThanOrEqual(tools[i-1].order); + } + }); + + test('searchTools finds tools by name and description', () => { + const results = ToolsManager.searchTools('image'); + expect(Array.isArray(results)).toBe(true); + expect(results.length).toBeGreaterThan(0); + results.forEach(tool => { + const matchesName = tool.name.toLowerCase().includes('image'); + const matchesDesc = tool.description.toLowerCase().includes('image'); + expect(matchesName || matchesDesc).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/js/features/about.js b/js/features/about.js new file mode 100644 index 0000000..52bdbb0 --- /dev/null +++ b/js/features/about.js @@ -0,0 +1,11 @@ +import { generateToolList } from '../utils/template-generator.js'; +import { initializeTheme } from '../utils/theme.js'; + +// Initialize theme +initializeTheme(); + +// Generate tool listings +const toolsContainer = document.getElementById('tools-container'); +if (toolsContainer) { + toolsContainer.innerHTML = generateToolList(); +} diff --git a/js/features/ascii-art.js b/js/features/ascii-art.js new file mode 100644 index 0000000..b4091e9 --- /dev/null +++ b/js/features/ascii-art.js @@ -0,0 +1,103 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { notifications } from '../utils/ui.js'; +import { STORAGE_KEYS, FILE_LIMITS, UI_CONSTANTS } from '../utils/constants.js'; +import { fileValidation } from '../utils/validation.js'; + +class AsciiArt extends BaseTool { + constructor() { + super(); + this.initializeElements(); + this.initializeState(); + this.setupEventListeners(); + } + + initializeElements() { + return { + // File input elements + dropZone: document.getElementById('drop-zone'), + fileInput: document.getElementById('file-input'), + previewContainer: document.getElementById('preview-container'), + previewImage: document.getElementById('preview-image'), + + // Control elements + widthInput: document.getElementById('width-input'), + charsetSelect: document.getElementById('charset-select'), + customCharsGroup: document.querySelector('.custom-chars-group'), + customCharsInput: document.getElementById('custom-chars'), + contrastInput: document.getElementById('contrast-input'), + brightnessInput: document.getElementById('brightness-input'), + invertCheckbox: document.getElementById('invert-checkbox'), + colorCheckbox: document.getElementById('color-checkbox'), + generateButton: document.getElementById('generate-button'), + + // Output elements + outputContainer: document.getElementById('output-container'), + asciiOutput: document.getElementById('ascii-output'), + copyButton: document.getElementById('copy-button'), + downloadButton: document.getElementById('download-button'), + shareButton: document.getElementById('share-button'), + + // Modal elements + shareModal: document.getElementById('share-modal'), + closeModalButton: document.querySelector('.close-modal'), + shareButtons: document.querySelectorAll('.share-button'), + + // Notification + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + charsets: { + standard: '@#$%=+~-.,', + blocks: '█▓▒░ ', + simple: '#. ', + dots: '●○• ' + }, + currentImage: null, + imageData: null, + asciiResult: '' + }; + } + + bindEvents() { + const { dropZone, fileInput, generateButton, copyButton, downloadButton, shareButton, closeModalButton } = this.elements; + + // File handling + dropZone.addEventListener('dragover', this.handleDragOver.bind(this)); + dropZone.addEventListener('drop', this.handleDrop.bind(this)); + dropZone.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', this.handleFileSelect.bind(this)); + + // Controls + generateButton.addEventListener('click', this.generateAscii.bind(this)); + copyButton.addEventListener('click', this.copyToClipboard.bind(this)); + downloadButton.addEventListener('click', this.downloadAscii.bind(this)); + shareButton.addEventListener('click', this.showShareModal.bind(this)); + closeModalButton.addEventListener('click', this.hideShareModal.bind(this)); + + // Keyboard shortcuts + this.addKeyboardShortcut('g', this.generateAscii.bind(this), { ctrl: true }); + this.addKeyboardShortcut('c', this.copyToClipboard.bind(this), { ctrl: true }); + this.addKeyboardShortcut('s', this.downloadAscii.bind(this), { ctrl: true }); + } + + initialize() { + // Hide output initially + this.elements.outputContainer.style.display = 'none'; + + // Set up charset select + this.populateCharsetSelect(); + + // Initialize custom chars group visibility + this.updateCustomCharsVisibility(); + } + + // ... rest of the class implementation ... +} + +// Initialize the ASCII art generator +document.addEventListener('DOMContentLoaded', () => { + new AsciiArt(); +}); diff --git a/js/features/color-palette.js b/js/features/color-palette.js new file mode 100644 index 0000000..65eaa40 --- /dev/null +++ b/js/features/color-palette.js @@ -0,0 +1,411 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { showNotification } from '../utils/ui.js'; +import { STORAGE_KEYS } from '../utils/constants.js'; + +export default class ColorPalette extends BaseTool { + constructor() { + super(); + this.elements = this.initializeElements(); + this.state = this.initializeState(); + this.initialize(); + this.bindEvents(); + } + + initializeElements() { + return { + colorInput: document.getElementById('color-input'), + colorPicker: document.getElementById('color-picker'), + paletteContainer: document.getElementById('palette-container'), + paletteList: document.getElementById('palette-list'), + generateButton: document.getElementById('generate-button'), + copyButton: document.getElementById('copy-button'), + clearButton: document.getElementById('clear-button'), + schemeSelect: document.getElementById('scheme-select'), + countSelect: document.getElementById('count-select'), + notification: document.querySelector('.notification'), + exportButtons: document.querySelectorAll('.export-menu button'), + savedPaletteGrid: document.getElementById('saved-palette-grid'), + clearSavedButton: document.getElementById('clear-saved') + }; + } + + initializeState() { + return { + currentColor: '#000000', + palette: [], + savedPalettes: [], + maxSavedPalettes: 20, + schemes: { + monochromatic: { count: [3, 5], label: 'Monochromatic' }, + analogous: { count: [3, 5], label: 'Analogous' }, + complementary: { count: [2, 4], label: 'Complementary' }, + triadic: { count: [3], label: 'Triadic' }, + tetradic: { count: [4], label: 'Tetradic' }, + splitComplementary: { count: [3], label: 'Split Complementary' } + } + }; + } + + bindEvents() { + const { colorInput, colorPicker, generateButton, copyButton, clearButton, + schemeSelect, exportButtons, clearSavedButton } = this.elements; + + colorInput.addEventListener('input', this.handleColorInput.bind(this)); + colorPicker.addEventListener('change', this.handleColorPicker.bind(this)); + generateButton.addEventListener('click', this.generatePalette.bind(this)); + copyButton.addEventListener('click', this.copyPalette.bind(this)); + clearButton.addEventListener('click', this.clearPalette.bind(this)); + schemeSelect.addEventListener('change', this.handleSchemeChange.bind(this)); + clearSavedButton?.addEventListener('click', this.clearSavedPalettes.bind(this)); + + exportButtons?.forEach(button => { + button.addEventListener('click', () => this.exportPalette(button.dataset.format)); + }); + + this.addKeyboardShortcut('g', this.generatePalette.bind(this), { ctrl: true }); + this.addKeyboardShortcut('c', this.copyPalette.bind(this), { ctrl: true }); + } + + initialize() { + this.elements.colorInput.value = this.state.currentColor; + this.elements.colorPicker.value = this.state.currentColor; + this.populateSchemeSelect(); + this.loadSavedPalettes(); + this.generatePalette(); + } + + handleColorInput(event) { + const color = event.target.value; + if (!this.isValidColor(color)) return; + + this.updateCurrentColor(color); + this.generatePalette(); + } + + handleColorPicker(event) { + const color = event.target.value; + if (!this.isValidColor(color)) return; + + this.updateCurrentColor(color); + this.generatePalette(); + } + + handleSchemeChange() { + const scheme = this.elements.schemeSelect.value; + const counts = this.state.schemes[scheme].count; + + this.elements.countSelect.innerHTML = counts + .map(count => ``) + .join(''); + + this.generatePalette(); + } + + updateCurrentColor(color) { + this.state.currentColor = color; + this.elements.colorInput.value = color; + this.elements.colorPicker.value = color; + } + + populateSchemeSelect() { + this.elements.schemeSelect.innerHTML = Object.entries(this.state.schemes) + .map(([value, { label }]) => ``) + .join(''); + } + + async generatePalette() { + const scheme = this.elements.schemeSelect.value; + const count = parseInt(this.elements.countSelect.value); + const color = this.state.currentColor; + + try { + this.state.palette = this.generateColors(color, scheme, count); + this.displayPalette(); + this.showNotification('Palette generated successfully'); + } catch (error) { + console.error('Error generating palette:', error); + this.showNotification('Failed to generate palette', 'error'); + } + } + + generateColors(baseColor, scheme, count) { + const hsl = this.hexToHSL(baseColor); + + const generators = { + monochromatic: () => this.generateMonochromatic(hsl, count), + analogous: () => this.generateAnalogous(hsl, count), + complementary: () => this.generateComplementary(hsl, count), + triadic: () => this.generateTriadic(hsl), + tetradic: () => this.generateTetradic(hsl), + splitComplementary: () => this.generateSplitComplementary(hsl) + }; + + const generator = generators[scheme]; + if (!generator) { + throw new Error(`Invalid color scheme: ${scheme}`); + } + + return generator().map(this.HSLToHex.bind(this)); + } + + displayPalette() { + if (!this.elements.paletteList) return; + + this.elements.paletteList.innerHTML = this.state.palette + .map(color => ` +
+ + ${color} + +
+ `) + .join(''); + + const colorItems = this.elements.paletteList.querySelectorAll('.color-value'); + colorItems.forEach(item => { + item.addEventListener('click', () => this.copyToClipboard(item.textContent.trim())); + item.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.copyToClipboard(item.textContent.trim()); + } + }); + }); + } + + exportPalette(format) { + if (!this.state.palette.length) return; + + const formatters = { + hex: () => this.state.palette.join(', '), + rgb: () => this.state.palette.map(this.hexToRGB).join(', '), + hsl: () => this.state.palette.map(hex => { + const { h, s, l } = this.hexToHSL(hex); + return `hsl(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%)`; + }).join(', '), + css: () => this.state.palette + .map((hex, i) => `--color-${i + 1}: ${hex};`) + .join('\n'), + sass: () => this.state.palette + .map((hex, i) => `$color-${i + 1}: ${hex};`) + .join('\n') + }; + + const formatter = formatters[format]; + if (!formatter) { + this.showNotification(`Invalid export format: ${format}`, 'error'); + return; + } + + this.copyToClipboard(formatter()); + this.showNotification(`Palette exported as ${format.toUpperCase()}`); + } + + savePalette() { + if (!this.state.palette.length) return; + + const palette = { + colors: this.state.palette, + timestamp: Date.now() + }; + + this.state.savedPalettes.unshift(palette); + if (this.state.savedPalettes.length > this.state.maxSavedPalettes) { + this.state.savedPalettes.pop(); + } + + this.saveToStorage('savedPalettes', this.state.savedPalettes); + this.updateSavedPalettes(); + this.showNotification('Palette saved successfully'); + } + + loadSavedPalettes() { + const savedPalettes = this.loadFromStorage('savedPalettes'); + if (savedPalettes) { + this.state.savedPalettes = savedPalettes; + this.updateSavedPalettes(); + } + } + + updateSavedPalettes() { + if (!this.elements.savedPaletteGrid) return; + + this.elements.savedPaletteGrid.innerHTML = this.state.savedPalettes + .map((palette, index) => this.createSavedPaletteElement(palette, index)) + .join(''); + + this.elements.clearSavedButton.style.display = + this.state.savedPalettes.length ? 'block' : 'none'; + } + + createSavedPaletteElement(palette, index) { + return ` +
+
+ ${palette.colors.map(color => ` +
+
+ `).join('')} +
+
+ + +
+
+ `; + } + + clearSavedPalettes() { + this.state.savedPalettes = []; + this.saveToStorage('savedPalettes', []); + this.updateSavedPalettes(); + this.showNotification('All saved palettes cleared'); + } + + copyPalette() { + const colors = this.state.palette.join(', '); + this.copyToClipboard(colors); + } + + clearPalette() { + this.state.palette = []; + this.displayPalette(); + this.showNotification('Palette cleared'); + } + + isValidColor(color) { + return /^#[0-9A-F]{6}$/i.test(color); + } + + hexToRGB(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgb(${r}, ${g}, ${b})`; + } + + hexToHSL(hex) { + let r = parseInt(hex.slice(1, 3), 16) / 255; + let g = parseInt(hex.slice(3, 5), 16) / 255; + let b = parseInt(hex.slice(5, 7), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return { h: h * 360, s: s * 100, l: l * 100 }; + } + + HSLToHex({ h, s, l }) { + h /= 360; + s /= 100; + l /= 100; + + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + let r, g, b; + if (s === 0) { + r = g = b = l; + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + const toHex = x => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + generateMonochromatic({ h, s, l }, count) { + const step = 100 / (count + 1); + return Array.from({ length: count }, (_, i) => ({ + h, + s, + l: Math.min(100, Math.max(0, step * (i + 1))) + })); + } + + generateAnalogous({ h, s, l }, count) { + const angle = 30; + const step = angle / (count - 1); + return Array.from({ length: count }, (_, i) => ({ + h: (h + step * i + 360) % 360, + s, + l + })); + } + + generateComplementary({ h, s, l }, count) { + const complement = (h + 180) % 360; + return count === 2 ? [ + { h, s, l }, + { h: complement, s, l } + ] : [ + { h, s, l }, + { h: complement, s, l }, + { h, s: s * 0.8, l: l * 1.1 }, + { h: complement, s: s * 0.8, l: l * 1.1 } + ]; + } + + generateTriadic({ h, s, l }) { + return [ + { h, s, l }, + { h: (h + 120) % 360, s, l }, + { h: (h + 240) % 360, s, l } + ]; + } + + generateTetradic({ h, s, l }) { + return [ + { h, s, l }, + { h: (h + 90) % 360, s, l }, + { h: (h + 180) % 360, s, l }, + { h: (h + 270) % 360, s, l } + ]; + } + + generateSplitComplementary({ h, s, l }) { + return [ + { h, s, l }, + { h: (h + 150) % 360, s, l }, + { h: (h + 210) % 360, s, l } + ]; + } +} + +// Initialize the tool if we're on the color palette page +if (document.querySelector('.color-palette-container')) { + new ColorPalette(); +} diff --git a/js/features/image-resizer.js b/js/features/image-resizer.js new file mode 100644 index 0000000..787c077 --- /dev/null +++ b/js/features/image-resizer.js @@ -0,0 +1,236 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { showNotification, formatFileSize } from '../utils/ui.js'; +import { FILE_LIMITS } from '../utils/constants.js'; + +export default class ImageResizer extends BaseTool { + constructor() { + super(); + this.elements = this.initializeElements(); + this.state = this.initializeState(); + this.initialize(); + this.bindEvents(); + } + + initializeElements() { + return { + dropZone: document.getElementById('drop-zone'), + fileInput: document.getElementById('file-input'), + previewContainer: document.getElementById('preview-container'), + previewImage: document.getElementById('preview-image'), + fileInfo: document.getElementById('file-info'), + changeImageBtn: document.getElementById('change-image'), + settingsPanel: document.getElementById('settings-panel'), + widthInput: document.getElementById('width'), + heightInput: document.getElementById('height'), + aspectRatioLock: document.getElementById('aspect-ratio-lock'), + formatSelect: document.getElementById('format'), + qualityInput: document.getElementById('quality'), + qualityValue: document.getElementById('quality-value'), + resizeButton: document.getElementById('resize-button'), + downloadButton: document.getElementById('download-button'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + currentImage: null, + originalDimensions: { width: 0, height: 0 }, + aspectRatio: 1, + isAspectRatioLocked: true, + resizedImage: null, + maxFileSize: FILE_LIMITS.IMAGE_MAX_SIZE, + allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] + }; + } + + bindEvents() { + const { dropZone, fileInput, changeImageBtn, widthInput, heightInput, aspectRatioLock, qualityInput, resizeButton, downloadButton } = this.elements; + + dropZone.addEventListener('dragover', this.handleDragOver.bind(this)); + dropZone.addEventListener('drop', this.handleDrop.bind(this)); + dropZone.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', this.handleFileSelect.bind(this)); + changeImageBtn.addEventListener('click', this.resetImage.bind(this)); + + widthInput.addEventListener('input', this.debounce(() => this.handleDimensionChange('width'), 300)); + heightInput.addEventListener('input', this.debounce(() => this.handleDimensionChange('height'), 300)); + aspectRatioLock.addEventListener('click', this.toggleAspectRatio.bind(this)); + + qualityInput.addEventListener('input', this.debounce(() => { + this.elements.qualityValue.textContent = `${qualityInput.value}%`; + }, 100)); + + resizeButton.addEventListener('click', this.resizeImage.bind(this)); + downloadButton.addEventListener('click', this.downloadImage.bind(this)); + + this.addKeyboardShortcut('s', this.downloadImage.bind(this), { ctrl: true }); + } + + initialize() { + this.elements.settingsPanel.style.display = 'none'; + this.elements.downloadButton.disabled = true; + this.elements.qualityValue.textContent = `${this.elements.qualityInput.value}%`; + } + + validateFile(file) { + if (!file) { + this.showNotification('No file selected', 'error'); + return false; + } + + if (!this.state.allowedTypes.includes(file.type)) { + this.showNotification('Please select a valid image file (JPEG, PNG, WebP, or GIF)', 'error'); + return false; + } + + if (file.size > this.state.maxFileSize) { + this.showNotification(`File size must be less than ${formatFileSize(this.state.maxFileSize)}`, 'error'); + return false; + } + + return true; + } + + handleDragOver(e) { + e.preventDefault(); + e.stopPropagation(); + this.elements.dropZone.classList.add('dragover'); + } + + handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + this.elements.dropZone.classList.remove('dragover'); + + const files = e.dataTransfer.files; + if (files.length > 0) { + this.processFile(files[0]); + } + } + + handleFileSelect() { + const files = this.elements.fileInput.files; + if (files.length > 0) { + this.processFile(files[0]); + } + } + + processFile(file) { + if (!this.validateFile(file)) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + this.state.currentImage = img; + this.state.originalDimensions = { + width: img.width, + height: img.height + }; + this.state.aspectRatio = img.width / img.height; + this.updatePreview(); + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); + + this.elements.fileInfo.textContent = `${file.name} (${formatFileSize(file.size)})`; + } + + updatePreview() { + this.elements.previewImage.src = this.state.currentImage.src; + this.elements.previewContainer.style.display = 'block'; + this.elements.settingsPanel.style.display = 'block'; + this.elements.dropZone.style.display = 'none'; + + this.elements.widthInput.value = this.state.originalDimensions.width; + this.elements.heightInput.value = this.state.originalDimensions.height; + } + + handleDimensionChange(dimension) { + if (!this.state.currentImage) return; + + const value = parseInt(this.elements[`${dimension}Input`].value); + if (this.state.isAspectRatioLocked) { + if (dimension === 'width') { + this.elements.heightInput.value = Math.round(value / this.state.aspectRatio); + } else { + this.elements.widthInput.value = Math.round(value * this.state.aspectRatio); + } + } + } + + toggleAspectRatio() { + this.state.isAspectRatioLocked = !this.state.isAspectRatioLocked; + this.elements.aspectRatioLock.innerHTML = ``; + } + + async resizeImage() { + if (!this.state.currentImage) return; + + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const width = parseInt(this.elements.widthInput.value); + const height = parseInt(this.elements.heightInput.value); + const quality = parseInt(this.elements.qualityInput.value) / 100; + const format = this.elements.formatSelect.value; + + canvas.width = width; + canvas.height = height; + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(this.state.currentImage, 0, 0, width, height); + + const blob = await new Promise(resolve => { + canvas.toBlob(resolve, `image/${format}`, quality); + }); + + if (this.state.resizedImage) { + URL.revokeObjectURL(this.state.resizedImage); + } + + this.state.resizedImage = URL.createObjectURL(blob); + this.elements.previewImage.src = this.state.resizedImage; + this.elements.downloadButton.disabled = false; + + this.showNotification('Image resized successfully'); + } catch (error) { + console.error('Resize error:', error); + this.showNotification('Error resizing image', 'error'); + } + } + + downloadImage() { + if (!this.state.resizedImage) return; + + const format = this.elements.formatSelect.value; + const link = document.createElement('a'); + link.href = this.state.resizedImage; + link.download = `resized.${format}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + resetImage() { + if (this.state.resizedImage) { + URL.revokeObjectURL(this.state.resizedImage); + } + + this.state.currentImage = null; + this.state.resizedImage = null; + this.elements.previewContainer.style.display = 'none'; + this.elements.settingsPanel.style.display = 'none'; + this.elements.dropZone.style.display = 'block'; + this.elements.downloadButton.disabled = true; + } +} + +// Initialize the tool if we're on the image resizer page +if (document.querySelector('.resizer-controls')) { + new ImageResizer(); +} diff --git a/js/features/password-generator.js b/js/features/password-generator.js new file mode 100644 index 0000000..8176447 --- /dev/null +++ b/js/features/password-generator.js @@ -0,0 +1,109 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { showNotification } from '../utils/ui.js'; +import { STORAGE_KEYS } from '../utils/constants.js'; + +class PasswordGenerator extends BaseTool { + constructor() { + super(); + this.initializeElements(); + this.initializeState(); + this.bindEvents(); + } + + initializeElements() { + return { + // Password display + passwordDisplay: document.getElementById('password-display'), + strengthMeter: document.getElementById('strength-meter'), + strengthText: document.getElementById('strength-text'), + + // Length control + lengthInput: document.getElementById('length'), + lengthValue: document.getElementById('length-value'), + + // Character options + uppercaseCheck: document.getElementById('uppercase'), + lowercaseCheck: document.getElementById('lowercase'), + numbersCheck: document.getElementById('numbers'), + symbolsCheck: document.getElementById('symbols'), + + // Action buttons + generateButton: document.getElementById('generate-button'), + copyButton: document.getElementById('copy-button'), + + // History elements + historyContainer: document.getElementById('history-container'), + historyList: document.getElementById('history-list'), + clearHistoryButton: document.getElementById('clear-history'), + + // Notification + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + currentPassword: '', + history: [], + maxHistory: 10, + defaultLength: 16, + minLength: 8, + maxLength: 128, + characterSets: { + uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + lowercase: 'abcdefghijklmnopqrstuvwxyz', + numbers: '0123456789', + symbols: '!@#$%^&*()_+-=[]{}|;:,.<>?' + } + }; + } + + bindEvents() { + const { lengthInput, uppercaseCheck, lowercaseCheck, numbersCheck, symbolsCheck, generateButton, copyButton, clearHistoryButton } = this.elements; + + // Length input events + lengthInput.addEventListener('input', this.updateLengthValue.bind(this)); + + // Character option events + uppercaseCheck.addEventListener('change', this.validateOptions.bind(this)); + lowercaseCheck.addEventListener('change', this.validateOptions.bind(this)); + numbersCheck.addEventListener('change', this.validateOptions.bind(this)); + symbolsCheck.addEventListener('change', this.validateOptions.bind(this)); + + // Button events + generateButton.addEventListener('click', this.generatePassword.bind(this)); + copyButton.addEventListener('click', this.copyToClipboard.bind(this)); + clearHistoryButton.addEventListener('click', this.clearHistory.bind(this)); + + // Keyboard shortcuts + this.addKeyboardShortcut('g', this.generatePassword.bind(this), { ctrl: true }); + this.addKeyboardShortcut('c', this.copyToClipboard.bind(this), { ctrl: true }); + } + + initialize() { + // Set initial values + this.elements.lengthInput.value = this.state.defaultLength; + this.elements.lengthValue.textContent = this.state.defaultLength; + + // Set min/max values + this.elements.lengthInput.min = this.state.minLength; + this.elements.lengthInput.max = this.state.maxLength; + + // Check default options + this.elements.uppercaseCheck.checked = true; + this.elements.lowercaseCheck.checked = true; + this.elements.numbersCheck.checked = true; + + // Load history + this.loadHistory(); + + // Generate initial password + this.generatePassword(); + } + + // ... rest of the class implementation ... +} + +// Initialize the password generator +const passwordGenerator = new PasswordGenerator(); +window.passwordGenerator = passwordGenerator; // Make it accessible for history item actions diff --git a/js/features/qr-code.js b/js/features/qr-code.js new file mode 100644 index 0000000..79c41c7 --- /dev/null +++ b/js/features/qr-code.js @@ -0,0 +1,180 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { showNotification } from '../utils/ui.js'; + +export default class QRCode extends BaseTool { + constructor() { + super(); + this.elements = this.initializeElements(); + this.state = this.initializeState(); + this.initialize(); + this.bindEvents(); + } + + initializeElements() { + return { + textInput: document.getElementById('text-input'), + errorCorrectionSelect: document.getElementById('error-correction'), + sizeInput: document.getElementById('size'), + colorInput: document.getElementById('color'), + backgroundInput: document.getElementById('background'), + generateButton: document.getElementById('generate-button'), + downloadButton: document.getElementById('download-button'), + clearButton: document.getElementById('clear-button'), + qrContainer: document.getElementById('qr-container'), + qrCanvas: document.getElementById('qr-canvas'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + qr: null, + options: { + errorCorrectionLevel: 'M', + size: 256, + color: '#000000', + background: '#ffffff' + } + }; + } + + bindEvents() { + const { textInput, errorCorrectionSelect, sizeInput, colorInput, backgroundInput, generateButton, downloadButton, clearButton } = this.elements; + + textInput.addEventListener('input', this.validateInput.bind(this)); + textInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') this.generateQRCode(); + }); + + errorCorrectionSelect.addEventListener('change', this.updateOptions.bind(this)); + sizeInput.addEventListener('input', this.updateOptions.bind(this)); + colorInput.addEventListener('input', this.updateOptions.bind(this)); + backgroundInput.addEventListener('input', this.updateOptions.bind(this)); + + generateButton.addEventListener('click', this.generateQRCode.bind(this)); + downloadButton.addEventListener('click', this.downloadQRCode.bind(this)); + clearButton.addEventListener('click', this.clearInput.bind(this)); + + this.addKeyboardShortcut('g', this.generateQRCode.bind(this), { ctrl: true }); + this.addKeyboardShortcut('d', this.downloadQRCode.bind(this), { ctrl: true }); + } + + initialize() { + this.elements.errorCorrectionSelect.value = this.state.options.errorCorrectionLevel; + this.elements.sizeInput.value = this.state.options.size; + this.elements.colorInput.value = this.state.options.color; + this.elements.backgroundInput.value = this.state.options.background; + this.elements.qrContainer.style.display = 'none'; + this.elements.generateButton.disabled = true; + this.elements.downloadButton.disabled = true; + } + + validateInput() { + const text = this.elements.textInput.value.trim(); + this.elements.generateButton.disabled = !text; + if (this.state.qr && !text) { + this.clearQRCode(); + } + } + + updateOptions() { + this.state.options = { + errorCorrectionLevel: this.elements.errorCorrectionSelect.value, + size: parseInt(this.elements.sizeInput.value), + color: this.elements.colorInput.value, + background: this.elements.backgroundInput.value + }; + + if (this.state.qr) { + this.generateQRCode(); + } + } + + async generateQRCode() { + const text = this.elements.textInput.value.trim(); + if (!text) return; + + try { + if (!this.state.qr) { + this.state.qr = new QRCodeStyling({ + width: this.state.options.size, + height: this.state.options.size, + type: 'canvas', + data: text, + dotsOptions: { + color: this.state.options.color, + type: 'square' + }, + backgroundOptions: { + color: this.state.options.background + }, + qrOptions: { + errorCorrectionLevel: this.state.options.errorCorrectionLevel + } + }); + } else { + this.state.qr.update({ + data: text, + width: this.state.options.size, + height: this.state.options.size, + dotsOptions: { + color: this.state.options.color + }, + backgroundOptions: { + color: this.state.options.background + }, + qrOptions: { + errorCorrectionLevel: this.state.options.errorCorrectionLevel + } + }); + } + + this.elements.qrContainer.innerHTML = ''; + await this.state.qr.append(this.elements.qrContainer); + this.elements.qrContainer.style.display = 'block'; + this.elements.downloadButton.disabled = false; + this.showNotification('QR code generated successfully'); + } catch (error) { + console.error('QR code generation error:', error); + this.showNotification('Failed to generate QR code', 'error'); + } + } + + async downloadQRCode() { + if (!this.state.qr) return; + + try { + const canvas = this.elements.qrContainer.querySelector('canvas'); + const dataUrl = canvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = dataUrl; + link.download = 'qr-code.png'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch (error) { + console.error('Download error:', error); + this.showNotification('Failed to download QR code', 'error'); + } + } + + clearInput() { + this.elements.textInput.value = ''; + this.validateInput(); + this.clearQRCode(); + } + + clearQRCode() { + if (this.state.qr) { + this.elements.qrContainer.innerHTML = ''; + this.elements.qrContainer.style.display = 'none'; + this.elements.downloadButton.disabled = true; + this.state.qr = null; + } + } +} + +// Initialize the tool if we're on the QR code page +if (document.querySelector('.qr-controls')) { + new QRCode(); +} diff --git a/js/features/text-to-speech.js b/js/features/text-to-speech.js new file mode 100644 index 0000000..f03efcc --- /dev/null +++ b/js/features/text-to-speech.js @@ -0,0 +1,259 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { showNotification } from '../utils/ui.js'; + +export class TextToSpeech extends BaseTool { + constructor() { + super('text-to-speech'); + this.synth = window.speechSynthesis; + this.utterance = null; + this.voices = []; + this.isPlaying = false; + this.isPaused = false; + this.progress = 0; + + // Initialize UI elements + this.textInput = document.querySelector('#text-input'); + this.voiceSelect = document.querySelector('#voice-select'); + this.rateInput = document.querySelector('#speed-slider'); + this.pitchInput = document.querySelector('#pitch-slider'); + this.volumeInput = document.querySelector('#volume-slider'); + this.playButton = document.querySelector('#speak-btn'); + // Preview is our "pause" equivalent for now based on available buttons, or we hide pause + // The UI has: Preview, Speak, Download. + // BaseTool expects standard controls but we need to map to existing UI + this.previewButton = document.querySelector('#preview-btn'); + this.downloadButton = document.querySelector('#download-btn'); + + // Elements not present in current HTML but required by logic: + // We will create dummy elements or update logic. Updating logic is better. + this.pauseButton = null; + this.stopButton = null; + this.progressBar = null; + this.progressText = null; + + // Bind event handlers + this.handleVoicesChanged = this.handleVoicesChanged.bind(this); + this.handlePlay = this.handlePlay.bind(this); + this.handlePause = this.handlePause.bind(this); + this.handleStop = this.handleStop.bind(this); + this.handleBoundaryEvent = this.handleBoundaryEvent.bind(this); + this.handleEndEvent = this.handleEndEvent.bind(this); + this.handleErrorEvent = this.handleErrorEvent.bind(this); + + // Initialize voices + this.initVoices(); + + // Initialize listeners + this.init(); + } + + async init() { + try { + // Add event listeners + if (this.playButton) this.playButton.addEventListener('click', this.handlePlay); + // We don't have a dedicated pause button in the new UI, map preview to pause/resume or speak? + // The UI has "Speak" and "Preview Voice". + // Let's assume Speak is Play. + + // this.pauseButton.addEventListener('click', this.handlePause); + // this.stopButton.addEventListener('click', this.handleStop); + + this.synth.addEventListener('voiceschanged', this.handleVoicesChanged); + + // Initialize voices + await this.loadVoices(); + + // Enable controls + this.enableControls(); + } catch (error) { + console.error('Failed to initialize Text-to-Speech:', error); + showNotification('Failed to initialize Text-to-Speech. Please try again.', 'error'); + } + } + + async loadVoices() { + return new Promise((resolve) => { + const voices = this.synth.getVoices(); + if (voices.length > 0) { + this.voices = voices; + this.populateVoiceList(); + resolve(); + } else { + this.synth.addEventListener('voiceschanged', () => { + this.voices = this.synth.getVoices(); + this.populateVoiceList(); + resolve(); + }, { once: true }); + } + }); + } + + populateVoiceList() { + // Clear existing options + this.voiceSelect.innerHTML = ''; + + // Add voices to select element + this.voices.forEach((voice, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = `${voice.name} (${voice.lang})`; + this.voiceSelect.appendChild(option); + }); + + // Select default voice + const defaultVoice = this.voices.findIndex(voice => voice.default); + if (defaultVoice !== -1) { + this.voiceSelect.value = defaultVoice; + } + } + + handleVoicesChanged() { + this.voices = this.synth.getVoices(); + this.populateVoiceList(); + } + + handlePlay() { + if (this.isPlaying) return; + + const text = this.textInput.value.trim(); + if (!text) { + showNotification('Please enter some text to speak.', 'warning'); + return; + } + + try { + // Create new utterance + this.utterance = new SpeechSynthesisUtterance(text); + + // Set voice + const selectedVoice = this.voices[this.voiceSelect.value]; + if (selectedVoice) { + this.utterance.voice = selectedVoice; + } + + // Set speech properties + this.utterance.rate = parseFloat(this.rateInput.value); + this.utterance.pitch = parseFloat(this.pitchInput.value); + this.utterance.volume = parseFloat(this.volumeInput.value); + + // Add event listeners + this.utterance.onboundary = this.handleBoundaryEvent; + this.utterance.onend = this.handleEndEvent; + this.utterance.onerror = this.handleErrorEvent; + + // Start speaking + this.synth.speak(this.utterance); + this.isPlaying = true; + this.isPaused = false; + + // Update UI + this.updatePlaybackState(); + } catch (error) { + console.error('Failed to start speech:', error); + showNotification('Failed to start speech. Please try again.', 'error'); + } + } + + handlePause() { + if (!this.isPlaying) return; + + if (this.isPaused) { + this.synth.resume(); + this.isPaused = false; + } else { + this.synth.pause(); + this.isPaused = true; + } + + this.updatePlaybackState(); + } + + handleStop() { + if (!this.isPlaying) return; + + this.synth.cancel(); + this.isPlaying = false; + this.isPaused = false; + this.progress = 0; + + this.updatePlaybackState(); + this.updateProgress(); + } + + handleBoundaryEvent(event) { + if (event.name === 'word') { + const text = this.textInput.value; + const wordCount = text.trim().split(/\s+/).length; + const currentWord = Math.ceil(event.charIndex / (text.length / wordCount)); + this.progress = (currentWord / wordCount) * 100; + this.updateProgress(); + } + } + + handleEndEvent() { + this.isPlaying = false; + this.isPaused = false; + this.progress = 100; + + this.updatePlaybackState(); + this.updateProgress(); + + showNotification('Speech completed successfully.', 'success'); + } + + handleErrorEvent(error) { + console.error('Speech synthesis error:', error); + showNotification('An error occurred during speech synthesis.', 'error'); + + this.isPlaying = false; + this.isPaused = false; + this.progress = 0; + + this.updatePlaybackState(); + this.updateProgress(); + } + + updatePlaybackState() { + if (this.playButton) this.playButton.disabled = this.isPlaying; + if (this.pauseButton) this.pauseButton.disabled = !this.isPlaying; + if (this.stopButton) this.stopButton.disabled = !this.isPlaying; + + // Update pause button text + if (this.pauseButton) { + this.pauseButton.textContent = this.isPaused ? 'Resume' : 'Pause'; + this.pauseButton.className = this.isPaused ? 'action-button resume' : 'action-button pause'; + } + } + + updateProgress() { + if (this.progressBar) this.progressBar.style.width = `${this.progress}%`; + if (this.progressText) this.progressText.textContent = `${Math.round(this.progress)}%`; + } + + enableControls() { + if (this.textInput) this.textInput.disabled = false; + if (this.voiceSelect) this.voiceSelect.disabled = false; + if (this.rateInput) this.rateInput.disabled = false; + if (this.pitchInput) this.pitchInput.disabled = false; + if (this.volumeInput) this.volumeInput.disabled = false; + if (this.playButton) this.playButton.disabled = false; + } + + destroy() { + // Remove event listeners + if (this.playButton) this.playButton.removeEventListener('click', this.handlePlay); + if (this.pauseButton) this.pauseButton.removeEventListener('click', this.handlePause); + if (this.stopButton) this.stopButton.removeEventListener('click', this.handleStop); + this.synth.removeEventListener('voiceschanged', this.handleVoicesChanged); + + // Stop any ongoing speech + if (this.isPlaying) { + this.synth.cancel(); + } + } +} + +// Initialize the tool if we're on the text-to-speech page +if (document.querySelector('.tts-container')) { + new TextToSpeech(); +} diff --git a/js/features/tools-manager.js b/js/features/tools-manager.js new file mode 100644 index 0000000..4d51717 --- /dev/null +++ b/js/features/tools-manager.js @@ -0,0 +1,157 @@ +import { TOOLS, TOOL_CATEGORIES, CATEGORIES } from '../config/tools.js'; + +/** + * Initialize the tools grid + */ +export function initializeTools() { + const toolsGrid = document.getElementById('tools-grid'); + if (!toolsGrid) return; + + // Add loading state + toolsGrid.innerHTML = Array(6).fill(0).map(() => ` +
+
+
+
+
+
+
+
+ `).join(''); + + // Generate tool cards with a slight delay to show loading state + setTimeout(() => { + try { + const toolCards = generateToolCards(); + toolsGrid.innerHTML = toolCards; + + // Add click handlers and animations + document.querySelectorAll('.tool-card').forEach((card, index) => { + // Add animation delay + card.style.animationDelay = `${index * 0.1}s`; + + // Add click handler + card.addEventListener('click', () => { + const toolId = card.dataset.toolId; + if (toolId) { + navigateToTool(toolId); + } + }); + + // Add hover effects for features + card.querySelectorAll('.tool-feature').forEach(feature => { + const tooltip = feature.querySelector('.feature-tooltip'); + if (tooltip) { + feature.addEventListener('mouseenter', () => { + tooltip.style.opacity = '1'; + tooltip.style.transform = 'translateY(0)'; + }); + feature.addEventListener('mouseleave', () => { + tooltip.style.opacity = '0'; + tooltip.style.transform = 'translateY(5px)'; + }); + } + }); + }); + } catch (error) { + console.error('Failed to generate tool cards:', error); + toolsGrid.innerHTML = ` +
+ +

Failed to load tools. Please try refreshing the page.

+ +
+ `; + } + }, 500); +} + +/** + * Generate HTML for tool cards + */ +function generateToolCards() { + return TOOLS.sort((a, b) => a.order - b.order) + .map(tool => { + const category = CATEGORIES[tool.category]; + const categoryStyle = `style="--category-color: ${category.color}"`; + + return ` +
+
+ +
+
+

${tool.name}

+

${tool.description}

+
+ + + ${category.name} + +
+ ${tool.features.map(feature => ` + + +
+ ${feature.name} +

${feature.description}

+
+
+ `).join('')} +
+
+
+
+ `; + }) + .join(''); +} + +/** + * Navigate to a tool page + * @param {string} toolId - Tool ID to navigate to + */ +function navigateToTool(toolId) { + const tool = getToolById(toolId); + if (tool) { + // Use strict relative path. + // From index.html (root), it is 'pages/tool.html'. + // From a subpage (e.g. pages/other.html), this logic is not usually called + // because tools grid is only on index.html. + // If we ever render grid on subpages, we need basePath. + + const isSubPage = window.location.pathname.includes('/pages/'); + const basePath = isSubPage ? '../' : ''; + const fullPath = `${basePath}${tool.path}`; + + window.location.href = fullPath; + } +} + +/** + * Get a tool by its ID + * @param {string} id - Tool ID + * @returns {Object|null} Tool object or null if not found + */ +export function getToolById(id) { + return TOOLS.find(tool => tool.id === id) || null; +} + +/** + * Get tools by category + * @param {string} category - Category ID + * @returns {Array} Array of tools in the category + */ +export function getToolsByCategory(category) { + return TOOLS.filter(tool => tool.category === category); +} + +/** + * Get all available tools + * @returns {Array} Array of all tools + */ +export function getAllTools() { + return TOOLS; +} diff --git a/js/features/url-shortener.js b/js/features/url-shortener.js new file mode 100644 index 0000000..fed6520 --- /dev/null +++ b/js/features/url-shortener.js @@ -0,0 +1,228 @@ +import { BaseTool } from '../utils/base-tool.js'; +import { showNotification, validateURL } from '../utils/ui.js'; + +export default class URLShortener extends BaseTool { + constructor() { + super(); + this.elements = this.initializeElements(); + this.state = this.initializeState(); + this.initialize(); + this.bindEvents(); + } + + initializeElements() { + return { + urlInput: document.getElementById('url-input'), + customSlugInput: document.getElementById('custom-slug'), + shortenButton: document.getElementById('shorten-button'), + copyButton: document.getElementById('copy-button'), + clearButton: document.getElementById('clear-button'), + resultContainer: document.getElementById('result-container'), + shortUrlDisplay: document.getElementById('short-url'), + qrCodeContainer: document.getElementById('qr-code'), + historyContainer: document.getElementById('history-container'), + historyList: document.getElementById('history-list'), + clearHistoryButton: document.getElementById('clear-history'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + history: [], + maxHistory: 10, + isProcessing: false, + apiEndpoint: 'https://api.tinyurl.com/create' + }; + } + + bindEvents() { + const { urlInput, shortenButton, copyButton, clearButton, clearHistoryButton } = this.elements; + + urlInput.addEventListener('input', this.validateInput.bind(this)); + urlInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !this.state.isProcessing) { + this.shortenURL(); + } + }); + + shortenButton.addEventListener('click', this.shortenURL.bind(this)); + copyButton.addEventListener('click', this.copyToClipboard.bind(this)); + clearButton.addEventListener('click', this.clearInput.bind(this)); + clearHistoryButton.addEventListener('click', this.clearHistory.bind(this)); + + this.addKeyboardShortcut('s', this.shortenURL.bind(this), { ctrl: true }); + this.addKeyboardShortcut('c', this.copyToClipboard.bind(this), { ctrl: true }); + } + + initialize() { + this.loadHistory(); + this.elements.resultContainer.style.display = 'none'; + this.elements.shortenButton.disabled = true; + this.elements.copyButton.disabled = true; + this.updateHistoryDisplay(); + } + + validateInput() { + const url = this.elements.urlInput.value.trim(); + const isValid = validateURL(url); + this.elements.shortenButton.disabled = !isValid || this.state.isProcessing; + return isValid; + } + + async shortenURL() { + if (!this.validateInput() || this.state.isProcessing) return; + + const url = this.elements.urlInput.value.trim(); + const customSlug = this.elements.customSlugInput.value.trim(); + + this.state.isProcessing = true; + this.elements.shortenButton.disabled = true; + + try { + const response = await fetch(this.state.apiEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url, + ...(customSlug && { alias: customSlug }) + }) + }); + + if (!response.ok) { + throw new Error('Failed to shorten URL'); + } + + const data = await response.json(); + this.handleShortenSuccess(data.data.tiny_url, url); + } catch (error) { + console.error('URL shortening error:', error); + this.showNotification('Failed to shorten URL', 'error'); + } finally { + this.state.isProcessing = false; + this.elements.shortenButton.disabled = !this.validateInput(); + } + } + + handleShortenSuccess(shortUrl, originalUrl) { + this.elements.shortUrlDisplay.value = shortUrl; + this.elements.resultContainer.style.display = 'block'; + this.elements.copyButton.disabled = false; + + this.addToHistory({ + original: originalUrl, + shortened: shortUrl, + timestamp: Date.now() + }); + + this.showNotification('URL shortened successfully'); + } + + addToHistory(entry) { + this.state.history.unshift(entry); + if (this.state.history.length > this.state.maxHistory) { + this.state.history.pop(); + } + this.saveHistory(); + this.updateHistoryDisplay(); + } + + updateHistoryDisplay() { + if (!this.elements.historyList) return; + + this.elements.historyList.innerHTML = ''; + this.state.history.forEach(entry => { + const item = this.createHistoryItem(entry); + this.elements.historyList.appendChild(item); + }); + + this.elements.historyContainer.style.display = + this.state.history.length ? 'block' : 'none'; + } + + createHistoryItem(entry) { + const item = document.createElement('div'); + item.className = 'history-item'; + + const urlInfo = document.createElement('div'); + urlInfo.className = 'url-info'; + urlInfo.innerHTML = ` + ${entry.shortened} + ${entry.original} + ${new Date(entry.timestamp).toLocaleString()} + `; + + const actions = document.createElement('div'); + actions.className = 'action-buttons'; + actions.innerHTML = ` + + + `; + + actions.querySelector('.copy-button').addEventListener('click', () => { + this.copyToClipboard(entry.shortened); + }); + + actions.querySelector('.delete-button').addEventListener('click', () => { + this.removeFromHistory(entry); + }); + + item.appendChild(urlInfo); + item.appendChild(actions); + return item; + } + + removeFromHistory(entry) { + this.state.history = this.state.history.filter(item => + item.timestamp !== entry.timestamp); + this.saveHistory(); + this.updateHistoryDisplay(); + } + + clearHistory() { + this.state.history = []; + this.saveHistory(); + this.updateHistoryDisplay(); + this.showNotification('History cleared'); + } + + loadHistory() { + const savedHistory = this.loadFromStorage('urlShortenerHistory'); + if (savedHistory) { + this.state.history = savedHistory; + this.updateHistoryDisplay(); + } + } + + saveHistory() { + this.saveToStorage('urlShortenerHistory', this.state.history); + } + + copyToClipboard() { + const shortUrl = this.elements.shortUrlDisplay.value; + if (!shortUrl) return; + + navigator.clipboard.writeText(shortUrl) + .then(() => this.showNotification('URL copied to clipboard')) + .catch(() => this.showNotification('Failed to copy URL', 'error')); + } + + clearInput() { + this.elements.urlInput.value = ''; + this.elements.customSlugInput.value = ''; + this.elements.resultContainer.style.display = 'none'; + this.elements.copyButton.disabled = true; + this.validateInput(); + } +} + +// Initialize the tool if we're on the URL shortener page +if (document.querySelector('.url-shortener')) { + new URLShortener(); +} diff --git a/js/image-resizer.js b/js/image-resizer.js deleted file mode 100644 index c7cae41..0000000 --- a/js/image-resizer.js +++ /dev/null @@ -1,46 +0,0 @@ -let cropper; -const fileInput = document.getElementById('file-input'); -const imageContainer = document.getElementById('image-container'); -const resizeButton = document.getElementById('resize-button'); -const downloadLink = document.getElementById('download-link'); -const widthInput = document.getElementById('width'); -const heightInput = document.getElementById('height'); - -fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - imageContainer.innerHTML = ``; - const image = document.getElementById('image'); - cropper = new Cropper(image, { - aspectRatio: NaN, - viewMode: 1, - }); - }; - reader.readAsDataURL(file); - } -}); - -resizeButton.addEventListener('click', () => { - if (cropper) { - resizeButton.disabled = true; - resizeButton.textContent = 'Processing...'; - - const width = parseInt(widthInput.value); - const height = parseInt(heightInput.value); - const canvas = cropper.getCroppedCanvas({ - width: width, - height: height - }); - - canvas.toBlob((blob) => { - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.download = 'resized-image.png'; - downloadLink.style.display = 'inline-block'; - resizeButton.disabled = false; - resizeButton.textContent = 'Resize Image'; - }); - } -}); diff --git a/js/utils/app.js b/js/utils/app.js new file mode 100644 index 0000000..2821f1e --- /dev/null +++ b/js/utils/app.js @@ -0,0 +1,93 @@ +/** + * Application initialization and common utilities + */ + +import { initializeTheme } from './theme.js'; +import { initializeTools } from '../features/tools-manager.js'; +import { injectLayout } from '../components/layout.js'; + +/** + * Initialize mobile menu functionality + */ +function initializeMobileMenu() { + const menuToggle = document.querySelector('.mobile-menu-toggle'); + const navLinks = document.querySelector('.nav-links'); + const navLinksItems = document.querySelectorAll('.nav-link'); + + if (!menuToggle || !navLinks) return; + + menuToggle.addEventListener('click', () => { + navLinks.classList.toggle('active'); + menuToggle.querySelector('i').classList.toggle('fa-bars'); + menuToggle.querySelector('i').classList.toggle('fa-times'); + document.body.classList.toggle('menu-open'); + }); + + // Close menu when clicking nav links + navLinksItems.forEach(link => { + link.addEventListener('click', () => { + navLinks.classList.remove('active'); + menuToggle.querySelector('i').classList.add('fa-bars'); + menuToggle.querySelector('i').classList.remove('fa-times'); + document.body.classList.remove('menu-open'); + }); + }); +} + +/** + * Initialize smooth scrolling for navigation links + */ +function initializeSmoothScroll() { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); + }); +} + +/** + * Initialize the application + */ +function initializeApp() { + // Inject Layout (Header/Footer) first + injectLayout(); + + // Initialize theme system + initializeTheme(); + + // Initialize mobile menu + initializeMobileMenu(); + + // Initialize smooth scroll + initializeSmoothScroll(); + + // Initialize tools grid + initializeTools(); + + // Add scroll-based animations + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('animate-in'); + } + }); + }, { + threshold: 0.1 + }); + + document.querySelectorAll('.feature-card, .tools-grid > *, .about-content, .contribute-content') + .forEach(el => { + el.classList.add('animate-on-scroll'); + observer.observe(el); + }); +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', initializeApp); diff --git a/js/utils/base-tool.js b/js/utils/base-tool.js new file mode 100644 index 0000000..a0b1d82 --- /dev/null +++ b/js/utils/base-tool.js @@ -0,0 +1,126 @@ +import { showNotification } from './ui.js'; + +export class BaseTool { + constructor(toolId) { + if (new.target === BaseTool) { + throw new Error('BaseTool is an abstract class and cannot be instantiated directly.'); + } + + this.toolId = toolId; + this.isInitialized = false; + } + + async initialize() { + // Override in child class + // Should perform any necessary initialization + } + + handleError(error, context = '') { + console.error(`${this.toolId} error${context ? ` (${context})` : ''}:`, error); + showNotification( + `An error occurred${context ? ` while ${context}` : ''}. Please try again.`, + 'error' + ); + } + + validateState() { + if (!this.isInitialized) { + throw new Error(`${this.toolId} is not initialized`); + } + } + + async copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + showNotification('Copied to clipboard!', 'success'); + } catch (error) { + this.handleError(error, 'copying to clipboard'); + } + } + + async readFromClipboard() { + try { + const text = await navigator.clipboard.readText(); + return text; + } catch (error) { + this.handleError(error, 'reading from clipboard'); + return null; + } + } + + downloadFile(content, filename, type = 'text/plain') { + try { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + this.handleError(error, 'downloading file'); + } + } + + formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; + } + + debounce(func, wait) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + } + + throttle(func, limit) { + let inThrottle; + return (...args) => { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; + } + + getRandomId() { + return `${this.toolId}-${Math.random().toString(36).substr(2, 9)}`; + } + + setLocalStorage(key, value) { + try { + const fullKey = `${this.toolId}-${key}`; + localStorage.setItem(fullKey, JSON.stringify(value)); + } catch (error) { + this.handleError(error, 'saving to local storage'); + } + } + + getLocalStorage(key) { + try { + const fullKey = `${this.toolId}-${key}`; + const value = localStorage.getItem(fullKey); + return value ? JSON.parse(value) : null; + } catch (error) { + this.handleError(error, 'reading from local storage'); + return null; + } + } + + removeLocalStorage(key) { + try { + const fullKey = `${this.toolId}-${key}`; + localStorage.removeItem(fullKey); + } catch (error) { + this.handleError(error, 'removing from local storage'); + } + } +} diff --git a/js/utils/dom.js b/js/utils/dom.js new file mode 100644 index 0000000..4ac17f8 --- /dev/null +++ b/js/utils/dom.js @@ -0,0 +1,87 @@ +/** + * DOM Utilities + * @module utils/dom + */ + +/** + * Sanitize HTML string to prevent XSS attacks + * @param {string} html - HTML string to sanitize + * @returns {string} Sanitized HTML string + */ +export function sanitizeHTML(html) { + const div = document.createElement('div'); + div.textContent = html; + return div.innerHTML; +} + +/** + * Create an element with attributes and children + * @param {string} tag - Element tag name + * @param {Object} [attrs={}] - Element attributes + * @param {Array} [children=[]] - Child elements or text + * @returns {HTMLElement} Created element + */ +export function createElement(tag, attrs = {}, children = []) { + const element = document.createElement(tag); + + Object.entries(attrs).forEach(([key, value]) => { + if (key === 'className') { + element.className = value; + } else if (key === 'dataset') { + Object.entries(value).forEach(([dataKey, dataValue]) => { + element.dataset[dataKey] = dataValue; + }); + } else if (key.startsWith('on') && typeof value === 'function') { + element.addEventListener(key.slice(2).toLowerCase(), value); + } else { + element.setAttribute(key, value); + } + }); + + children.forEach(child => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + element.appendChild(child); + } + }); + + return element; +} + +/** + * Add multiple event listeners to an element + * @param {HTMLElement} element - Target element + * @param {Object} listeners - Event listeners object + */ +export function addEventListeners(element, listeners) { + Object.entries(listeners).forEach(([event, callback]) => { + element.addEventListener(event, callback); + }); +} + +/** + * Remove multiple event listeners from an element + * @param {HTMLElement} element - Target element + * @param {Object} listeners - Event listeners object + */ +export function removeEventListeners(element, listeners) { + Object.entries(listeners).forEach(([event, callback]) => { + element.removeEventListener(event, callback); + }); +} + +/** + * Check if an element is visible in viewport + * @param {HTMLElement} element - Element to check + * @returns {boolean} Whether element is visible + */ +export function isInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} diff --git a/js/utils/format.js b/js/utils/format.js new file mode 100644 index 0000000..2787a08 --- /dev/null +++ b/js/utils/format.js @@ -0,0 +1,104 @@ +/** + * Formatting utilities for various data types + */ + +/** + * Format date with options + * @param {Date|string|number} date - Date to format + * @param {Intl.DateTimeFormatOptions} options - Format options + * @returns {string} Formatted date + */ +export function formatDate(date, options = {}) { + const dateObj = new Date(date); + return new Intl.DateTimeFormat(undefined, options).format(dateObj); +} + +/** + * Format relative time (e.g., "2 hours ago") + * @param {Date|string|number} date - Date to format + * @returns {string} Relative time string + */ +export function formatRelativeTime(date) { + const now = new Date(); + const then = new Date(date); + const diff = now.getTime() - then.getTime(); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 7) { + return formatDate(then); + } else if (days > 0) { + return `${days} day${days === 1 ? '' : 's'} ago`; + } else if (hours > 0) { + return `${hours} hour${hours === 1 ? '' : 's'} ago`; + } else if (minutes > 0) { + return `${minutes} minute${minutes === 1 ? '' : 's'} ago`; + } else { + return 'Just now'; + } +} + +/** + * Format file size in bytes to human readable format + * @param {number} bytes - Size in bytes + * @returns {string} Formatted size + */ +export function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +} + +/** + * Get file extension from filename + * @param {string} filename - Filename + * @returns {string} File extension + */ +export function getFileExtension(filename) { + return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2); +} + +/** + * Convert RGB to Hex color + * @param {number} r - Red (0-255) + * @param {number} g - Green (0-255) + * @param {number} b - Blue (0-255) + * @returns {string} Hex color + */ +export function rgbToHex(r, g, b) { + return '#' + [r, g, b].map(x => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); +} + +/** + * Convert Hex to RGB color + * @param {string} hex - Hex color + * @returns {{r: number, g: number, b: number}|null} RGB color or null if invalid + */ +export function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +/** + * Check if color is light + * @param {string} color - Hex color + * @returns {boolean} Whether color is light + */ +export function isLightColor(color) { + const rgb = hexToRgb(color); + if (!rgb) return false; + const { r, g, b } = rgb; + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness > 128; +} \ No newline at end of file diff --git a/js/utils/storage.js b/js/utils/storage.js new file mode 100644 index 0000000..19d3c88 --- /dev/null +++ b/js/utils/storage.js @@ -0,0 +1,88 @@ +/** + * Storage utilities for managing local and session storage + */ + +/** + * Check if storage type is available + * @param {string} type - Storage type ('localStorage' or 'sessionStorage') + * @returns {boolean} Whether storage is available + */ +export function isStorageAvailable(type) { + try { + const storage = window[type]; + const x = '__storage_test__'; + storage.setItem(x, x); + storage.removeItem(x); + return true; + } catch (e) { + return false; + } +} + +/** + * Get item from storage + * @param {string} key - Storage key + * @param {'local'|'session'} type - Storage type + * @returns {any} Stored value or null + */ +export function getStorageItem(key, type = 'local') { + try { + const storage = type === 'local' ? localStorage : sessionStorage; + const item = storage.getItem(key); + return item ? JSON.parse(item) : null; + } catch (error) { + console.error(`Error getting ${key} from ${type} storage:`, error); + return null; + } +} + +/** + * Set item in storage + * @param {string} key - Storage key + * @param {any} value - Value to store + * @param {'local'|'session'} type - Storage type + * @returns {boolean} Whether operation was successful + */ +export function setStorageItem(key, value, type = 'local') { + try { + const storage = type === 'local' ? localStorage : sessionStorage; + storage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + console.error(`Error setting ${key} in ${type} storage:`, error); + return false; + } +} + +/** + * Remove item from storage + * @param {string} key - Storage key + * @param {'local'|'session'} type - Storage type + * @returns {boolean} Whether operation was successful + */ +export function removeStorageItem(key, type = 'local') { + try { + const storage = type === 'local' ? localStorage : sessionStorage; + storage.removeItem(key); + return true; + } catch (error) { + console.error(`Error removing ${key} from ${type} storage:`, error); + return false; + } +} + +/** + * Clear all items from storage + * @param {'local'|'session'} type - Storage type + * @returns {boolean} Whether operation was successful + */ +export function clearStorage(type = 'local') { + try { + const storage = type === 'local' ? localStorage : sessionStorage; + storage.clear(); + return true; + } catch (error) { + console.error(`Error clearing ${type} storage:`, error); + return false; + } +} \ No newline at end of file diff --git a/js/utils/template-generator.js b/js/utils/template-generator.js new file mode 100644 index 0000000..be48db8 --- /dev/null +++ b/js/utils/template-generator.js @@ -0,0 +1,135 @@ +import { getToolById } from '../features/tools-manager.js'; +import { CATEGORIES } from '../config/tools.js'; + +/** + * Generate tool page template + * @param {string} toolId - Tool ID + * @returns {string} Tool page HTML + */ +export function generateToolPage(toolId) { + const tool = getToolById(toolId); + if (!tool) { + return generateErrorPage('Tool not found'); + } + + const category = CATEGORIES[tool.category]; + return ` +
+ +
+
+
+ +
+

${tool.name}

+

${tool.description}

+
+ ${tool.features.map(feature => ` + + + ${feature.name} + + `).join('')} +
+
+
+ + +
+
+ +
+
+
+ + + +
+ + + +
+
+ `; +} + +/** + * Generate error page template + * @param {string} message - Error message + * @returns {string} Error page HTML + */ +function generateErrorPage(message) { + return ` +
+
+ +

Oops!

+

${message}

+ + + Back to Home + +
+
+ `; +} + +/** + * Share functions + */ +window.shareOnTwitter = function() { + const url = encodeURIComponent(window.location.href); + const text = encodeURIComponent(document.title); + window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank'); +}; + +window.shareOnFacebook = function() { + const url = encodeURIComponent(window.location.href); + window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank'); +}; + +window.shareOnReddit = function() { + const url = encodeURIComponent(window.location.href); + const title = encodeURIComponent(document.title); + window.open(`https://reddit.com/submit?url=${url}&title=${title}`, '_blank'); +}; + +window.copyToolLink = function() { + const button = document.querySelector('.share-button.copy-link'); + navigator.clipboard.writeText(window.location.href).then(() => { + const originalText = button.innerHTML; + button.innerHTML = ' Copied!'; + button.classList.add('success'); + setTimeout(() => { + button.innerHTML = originalText; + button.classList.remove('success'); + }, 2000); + }); +}; diff --git a/js/utils/theme.js b/js/utils/theme.js new file mode 100644 index 0000000..24aecc9 --- /dev/null +++ b/js/utils/theme.js @@ -0,0 +1,152 @@ +/** + * Theme system initialization and management + */ + +// Theme constants +const THEMES = { + LIGHT: 'light', + DARK: 'dark', + SYSTEM: 'system' +}; + +// Theme manager class +class ThemeManager { + constructor() { + this.theme = THEMES.SYSTEM; + this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + this.observers = new Set(); + } + + initialize() { + // Load saved theme + const savedTheme = localStorage.getItem('theme'); + if (savedTheme && Object.values(THEMES).includes(savedTheme)) { + this.theme = savedTheme; + } + + // Initialize theme + this.applyTheme(); + + // Listen for system theme changes + this.mediaQuery.addEventListener('change', () => { + if (this.theme === THEMES.SYSTEM) { + this.applyTheme(); + } + }); + + // Initialize theme toggle + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', () => this.toggleTheme()); + this.updateToggleButton(themeToggle); + } + } + + applyTheme() { + let effectiveTheme = this.theme; + + // If system theme, use system preference + if (effectiveTheme === THEMES.SYSTEM) { + effectiveTheme = this.mediaQuery.matches ? THEMES.DARK : THEMES.LIGHT; + } + + // Apply theme to document + document.documentElement.setAttribute('data-theme', effectiveTheme); + + // Update meta theme color + const metaThemeColor = document.querySelector('meta[name="theme-color"]'); + if (metaThemeColor) { + metaThemeColor.setAttribute( + 'content', + effectiveTheme === THEMES.DARK ? '#1a1a1a' : '#ffffff' + ); + } + + // Notify observers + this.notifyObservers(); + } + + toggleTheme() { + const currentTheme = this.getEffectiveTheme(); + this.setTheme(currentTheme === THEMES.LIGHT ? THEMES.DARK : THEMES.LIGHT); + } + + setTheme(theme) { + if (!Object.values(THEMES).includes(theme)) { + console.error(`Invalid theme: ${theme}`); + return; + } + + this.theme = theme; + localStorage.setItem('theme', theme); + this.applyTheme(); + + // Update toggle button if it exists + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + this.updateToggleButton(themeToggle); + } + } + + getTheme() { + return this.theme; + } + + getEffectiveTheme() { + if (this.theme === THEMES.SYSTEM) { + return this.mediaQuery.matches ? THEMES.DARK : THEMES.LIGHT; + } + return this.theme; + } + + updateToggleButton(button) { + const isDark = this.getEffectiveTheme() === THEMES.DARK; + button.setAttribute('aria-label', `Switch to ${isDark ? 'light' : 'dark'} theme`); + button.innerHTML = ` + + ${isDark ? 'Light' : 'Dark'} Mode + `; + } + + addObserver(callback) { + this.observers.add(callback); + } + + removeObserver(callback) { + this.observers.delete(callback); + } + + notifyObservers() { + const effectiveTheme = this.getEffectiveTheme(); + this.observers.forEach(callback => callback(effectiveTheme)); + } +} + +// Create and export theme manager instance +const themeManager = new ThemeManager(); + +export function initializeTheme() { + themeManager.initialize(); +} + +export function setTheme(theme) { + themeManager.setTheme(theme); +} + +export function getTheme() { + return themeManager.getTheme(); +} + +export function getEffectiveTheme() { + return themeManager.getEffectiveTheme(); +} + +export function addThemeObserver(callback) { + themeManager.addObserver(callback); +} + +export function removeThemeObserver(callback) { + themeManager.removeObserver(callback); +} + +export { THEMES }; diff --git a/js/utils/ui.js b/js/utils/ui.js new file mode 100644 index 0000000..3e84d56 --- /dev/null +++ b/js/utils/ui.js @@ -0,0 +1,377 @@ +/** + * UI utilities for Digital Services Hub + */ + +import { UI_CONSTANTS, THEMES } from './constants.js'; + +// Notification container +let notificationContainer = null; + +// Create notification container +function createNotificationContainer() { + if (notificationContainer) return; + + notificationContainer = document.createElement('div'); + notificationContainer.className = 'notification-container'; + notificationContainer.setAttribute('role', 'alert'); + notificationContainer.setAttribute('aria-live', 'polite'); + document.body.appendChild(notificationContainer); +} + +// Show notification +export function showNotification(message, type = 'info', duration = 3000) { + createNotificationContainer(); + + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.setAttribute('role', 'alert'); + notification.setAttribute('aria-atomic', 'true'); + + const icon = document.createElement('i'); + icon.className = getNotificationIcon(type); + notification.appendChild(icon); + + const text = document.createElement('span'); + text.textContent = message; + notification.appendChild(text); + + const closeButton = document.createElement('button'); + closeButton.className = 'notification-close'; + closeButton.setAttribute('aria-label', 'Close notification'); + closeButton.innerHTML = ''; + closeButton.addEventListener('click', () => removeNotification(notification)); + notification.appendChild(closeButton); + + notificationContainer.appendChild(notification); + + // Trigger animation + requestAnimationFrame(() => { + notification.classList.add('notification-show'); + }); + + // Auto remove after duration + if (duration > 0) { + setTimeout(() => removeNotification(notification), duration); + } + + return notification; +} + +// Remove notification +function removeNotification(notification) { + notification.classList.remove('notification-show'); + notification.addEventListener('transitionend', () => { + notification.remove(); + if (notificationContainer && notificationContainer.children.length === 0) { + notificationContainer.remove(); + notificationContainer = null; + } + }); +} + +// Get notification icon based on type +function getNotificationIcon(type) { + switch (type) { + case 'success': + return 'fas fa-check-circle'; + case 'error': + return 'fas fa-exclamation-circle'; + case 'warning': + return 'fas fa-exclamation-triangle'; + default: + return 'fas fa-info-circle'; + } +} + +// Create loading spinner +export function createLoadingSpinner(container, size = 'medium', text = 'Loading...') { + const spinner = document.createElement('div'); + spinner.className = `loading-spinner loading-spinner-${size}`; + spinner.setAttribute('role', 'status'); + spinner.setAttribute('aria-label', text); + + const spinnerInner = document.createElement('div'); + spinnerInner.className = 'loading-spinner-inner'; + spinner.appendChild(spinnerInner); + + if (text) { + const spinnerText = document.createElement('div'); + spinnerText.className = 'loading-spinner-text'; + spinnerText.textContent = text; + spinner.appendChild(spinnerText); + } + + if (container) { + container.appendChild(spinner); + } + + return spinner; +} + +// Remove loading spinner +export function removeLoadingSpinner(spinner) { + if (spinner && spinner.parentNode) { + spinner.remove(); + } +} + +// Create modal +export function createModal(options = {}) { + const { + title = '', + content = '', + buttons = [], + size = 'medium', + closeOnOverlayClick = true, + showCloseButton = true + } = options; + + const modal = document.createElement('div'); + modal.className = `modal modal-${size}`; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-labelledby', 'modal-title'); + + const modalContent = document.createElement('div'); + modalContent.className = 'modal-content'; + + if (title) { + const modalHeader = document.createElement('div'); + modalHeader.className = 'modal-header'; + + const modalTitle = document.createElement('h2'); + modalTitle.id = 'modal-title'; + modalTitle.className = 'modal-title'; + modalTitle.textContent = title; + modalHeader.appendChild(modalTitle); + + if (showCloseButton) { + const closeButton = document.createElement('button'); + closeButton.className = 'modal-close'; + closeButton.setAttribute('aria-label', 'Close modal'); + closeButton.innerHTML = ''; + closeButton.addEventListener('click', () => closeModal(modal)); + modalHeader.appendChild(closeButton); + } + + modalContent.appendChild(modalHeader); + } + + const modalBody = document.createElement('div'); + modalBody.className = 'modal-body'; + if (typeof content === 'string') { + modalBody.innerHTML = content; + } else { + modalBody.appendChild(content); + } + modalContent.appendChild(modalBody); + + if (buttons.length > 0) { + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + buttons.forEach(button => { + const btn = document.createElement('button'); + btn.className = `btn btn-${button.type || 'secondary'}`; + btn.textContent = button.text; + if (button.onClick) { + btn.addEventListener('click', () => button.onClick(modal)); + } + modalFooter.appendChild(btn); + }); + + modalContent.appendChild(modalFooter); + } + + modal.appendChild(modalContent); + + const modalOverlay = document.createElement('div'); + modalOverlay.className = 'modal-overlay'; + if (closeOnOverlayClick) { + modalOverlay.addEventListener('click', () => closeModal(modal)); + } + + const modalWrapper = document.createElement('div'); + modalWrapper.className = 'modal-wrapper'; + modalWrapper.appendChild(modalOverlay); + modalWrapper.appendChild(modal); + + document.body.appendChild(modalWrapper); + + // Trigger animation + requestAnimationFrame(() => { + modalWrapper.classList.add('modal-show'); + }); + + return modal; +} + +// Close modal +export function closeModal(modal) { + const modalWrapper = modal.closest('.modal-wrapper'); + modalWrapper.classList.remove('modal-show'); + modalWrapper.addEventListener('transitionend', () => { + modalWrapper.remove(); + }); +} + +// Create tooltip +export function createTooltip(element, text, position = 'top') { + const tooltip = document.createElement('div'); + tooltip.className = `tooltip tooltip-${position}`; + tooltip.setAttribute('role', 'tooltip'); + tooltip.textContent = text; + + element.addEventListener('mouseenter', () => { + document.body.appendChild(tooltip); + const rect = element.getBoundingClientRect(); + positionTooltip(tooltip, rect, position); + requestAnimationFrame(() => { + tooltip.classList.add('tooltip-show'); + }); + }); + + element.addEventListener('mouseleave', () => { + tooltip.classList.remove('tooltip-show'); + tooltip.addEventListener('transitionend', () => { + if (tooltip.parentNode) { + tooltip.remove(); + } + }); + }); + + return tooltip; +} + +// Position tooltip +function positionTooltip(tooltip, targetRect, position) { + const tooltipRect = tooltip.getBoundingClientRect(); + const spacing = 8; + + let top, left; + + switch (position) { + case 'top': + top = targetRect.top - tooltipRect.height - spacing; + left = targetRect.left + (targetRect.width - tooltipRect.width) / 2; + break; + case 'bottom': + top = targetRect.bottom + spacing; + left = targetRect.left + (targetRect.width - tooltipRect.width) / 2; + break; + case 'left': + top = targetRect.top + (targetRect.height - tooltipRect.height) / 2; + left = targetRect.left - tooltipRect.width - spacing; + break; + case 'right': + top = targetRect.top + (targetRect.height - tooltipRect.height) / 2; + left = targetRect.right + spacing; + break; + } + + tooltip.style.top = `${top}px`; + tooltip.style.left = `${left}px`; +} + +// Create confirmation dialog +export function createConfirmDialog(options = {}) { + const { + title = 'Confirm', + message = 'Are you sure?', + confirmText = 'Confirm', + cancelText = 'Cancel', + confirmType = 'primary', + onConfirm, + onCancel + } = options; + + return createModal({ + title, + content: message, + buttons: [ + { + text: cancelText, + type: 'secondary', + onClick: (modal) => { + closeModal(modal); + if (onCancel) onCancel(); + } + }, + { + text: confirmText, + type: confirmType, + onClick: (modal) => { + closeModal(modal); + if (onConfirm) onConfirm(); + } + } + ] + }); +} + +// Toggle element visibility +export function toggleVisibility(element, show) { + if (show) { + element.classList.remove('hidden'); + element.setAttribute('aria-hidden', 'false'); + } else { + element.classList.add('hidden'); + element.setAttribute('aria-hidden', 'true'); + } +} + +// Format file size +export function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +} + +// Format date +export function formatDate(date, options = {}) { + return new Intl.DateTimeFormat('en-US', options).format(date); +} + +// Format number +export function formatNumber(number, options = {}) { + return new Intl.NumberFormat('en-US', options).format(number); +} + +// Validate email +export function validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); +} + +// Validate URL +export function validateURL(url) { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +// Debounce function +export function debounce(func, wait) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +// Throttle function +export function throttle(func, limit) { + let inThrottle; + return (...args) => { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5e0f817 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "digital-services-hub", + "version": "1.0.0", + "description": "A collection of digital tools and utilities", + "type": "module", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint js/", + "lint:fix": "eslint js/ --fix", + "start": "serve ." + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1" + }, + "devDependencies": { + "@babel/core": "^7.23.6", + "@babel/preset-env": "^7.23.6", + "babel-jest": "^29.7.0", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "serve": "^14.2.1" + }, + "jest": { + "transform": { + "^.+\\.js$": "babel-jest" + }, + "testEnvironment": "node", + "moduleNameMapper": { + "^@/(.*)$": "/js/$1" + } + } +} diff --git a/pages/about.html b/pages/about.html deleted file mode 100644 index 6cc573c..0000000 --- a/pages/about.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - About - Digital Services Hub - - - -
-

About Our Services

- - - -
-

Welcome to our Digital Services Hub! We offer a range of tools to help with your digital media needs.

- -

Our Tools:

-
    -
  • Futuristic Image Resizer: Quickly resize your images with our sleek tool.
  • -
  • Color Palette Generator: Extract color palettes from your favorite images.
  • -
- -

All our tools are free to use and designed with a futuristic aesthetic. We're constantly working on adding new features and tools to enhance your digital workflow.

-
-
- - - - diff --git a/pages/ascii-art.html b/pages/ascii-art.html new file mode 100644 index 0000000..adbcbb8 --- /dev/null +++ b/pages/ascii-art.html @@ -0,0 +1,195 @@ + + + + + + ASCII Art Generator - Digital Services Hub + + + + + + + + + + +
+
+

ASCII Art Generator

+

Transform your images into text-based art

+
+
+ +
+
+ +
+ +
+ +

Drop an image here or click to upload

+

Supports: JPG, PNG, GIF (max 5MB)

+
+
+ + +
+ Preview of the uploaded image +
+ + +
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+ + + + + +
+

+
+                
+ + + +
+
+
+
+ + + + + + + + + + + + + diff --git a/pages/color-palette.html b/pages/color-palette.html index 0a4ade2..dc7178e 100644 --- a/pages/color-palette.html +++ b/pages/color-palette.html @@ -4,32 +4,154 @@ Color Palette Generator - Digital Services Hub + - + + + + -
-

Color Palette Generator

- - - -
-
- - + + +
+
+

Color Palette Generator

+

Create harmonious color schemes for your designs

+
+
+ +
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + +
-
- -
+ +
+
+

Generated Palette

+
+ +
+ + +
+
+
+
+
- + +
+
+

Saved Palettes

+ +
+
+
+
+
-

Upload an image and click 'Generate Palette' to extract colors.

+ + - - + + + + + + + + + diff --git a/pages/image-resizer.html b/pages/image-resizer.html index 62edf76..e56a2cf 100644 --- a/pages/image-resizer.html +++ b/pages/image-resizer.html @@ -3,44 +3,106 @@ - Futuristic Image Resizer - Digital Services Hub + Image Resizer - Digital Services Hub + - - - + + + + -
-

Futuristic Image Resizer

- - - -
-
- - -
- -
- -
- - + + +
+
+

Image Resizer

+

Resize and optimize your images with precision

+
+
+ +
+
+ +
+ +
+ +

Drop image here or click to upload

+

Supports: JPG, PNG, WebP, GIF

+
- -
- - + + + - - - -

Click 'Resize Image' to process, then 'Download' to save your image.

+ +
-
+
+ + + + - - + + diff --git a/pages/password-generator.html b/pages/password-generator.html new file mode 100644 index 0000000..31447bb --- /dev/null +++ b/pages/password-generator.html @@ -0,0 +1,162 @@ + + + + + + Password Generator - Digital Services Hub + + + + + + + + + + +
+
+

Password Generator

+

Create strong, secure passwords instantly

+
+
+ +
+
+ +
+
+ +
+ + +
+
+
+
Strength: None
+
+
+
+
+
+
+
+
+ + +
+
+ +
+ + 16 +
+
+ +
+

Character Types

+
+ + + + +
+
+ +
+

Advanced Options

+
+ + +
+ + +
+
+
+
+ + +
+

Password Requirements

+
    +
  • + 8-64 characters long +
  • +
  • + Contains uppercase letter +
  • +
  • + Contains lowercase letter +
  • +
  • + Contains number +
  • +
  • + Contains symbol +
  • +
+
+ + +
+
+

Recently Generated

+ +
+
+
+
+
+ + + + + + + + + + diff --git a/pages/qr-code.html b/pages/qr-code.html new file mode 100644 index 0000000..0fe38f8 --- /dev/null +++ b/pages/qr-code.html @@ -0,0 +1,113 @@ + + + + + + QR Code Generator - Digital Services Hub + + + + + + + + + + +
+
+

QR Code Generator

+

Create customizable QR codes for your links and data

+
+
+ +
+
+
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + +
+ + +
+ +
+
+
+
+ + + + + + + + + + + + + diff --git a/pages/text-to-speech.html b/pages/text-to-speech.html new file mode 100644 index 0000000..9ac87bf --- /dev/null +++ b/pages/text-to-speech.html @@ -0,0 +1,134 @@ + + + + + + Text to Speech - Digital Services Hub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Text to Speech

+
+ + + +
+
+ + + +
+
Detected Language: None
+
0 / 5000 characters
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+ +
+ + + + + + +
+
+ + + + + + + diff --git a/pages/url-shortener.html b/pages/url-shortener.html new file mode 100644 index 0000000..03e4ea4 --- /dev/null +++ b/pages/url-shortener.html @@ -0,0 +1,141 @@ + + + + + + URL Shortener - Digital Services Hub + + + + + + + + + + +
+
+

URL Shortener

+

Create short, memorable links instantly

+
+
+ +
+
+
+
+ + +
+ +
+
+ + Create a memorable custom link (optional) +
+ +
+ + +
+
+
+ + + + +
+ + + +
+

Features

+
+
+ +

Click Analytics

+

Track the performance of your shortened URLs with detailed click statistics.

+
+
+ +

QR Code Generation

+

Generate QR codes for your shortened URLs instantly.

+
+
+ +

Custom Expiry

+

Set custom expiration dates for your shortened URLs.

+
+
+ +

Custom Aliases

+

Create memorable custom aliases for your links.

+
+
+
+
+ + + + + + +