diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..acbc2dc --- /dev/null +++ b/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/index.html b/index.html index 2489afa..223d59c 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,11 @@ - QR Generator + Simple QR Generator - + + @@ -68,6 +69,46 @@ + +
+ ADVANCED CONFIGURATION + +
+ + +
+ +
+ + + + Min: 64px // Max: 4000px + +
+ +
+
+ +
+ + #000000 +
+
+ +
+ +
+ + #FFFFFF +
+
+
+
@@ -77,6 +118,20 @@
+ + diff --git a/script.js b/script.js index 507b17d..afea26f 100644 --- a/script.js +++ b/script.js @@ -1,137 +1,130 @@ document.addEventListener('DOMContentLoaded', () => { - // 1. Initialization - const modeSelector = document.getElementById('mode-selector'); + // --- GLOBALS --- const qrContainer = document.getElementById('qr-container'); const downloadBtn = document.getElementById('btn-download'); - // Forms - const forms = { - text: document.getElementById('form-text'), - wifi: document.getElementById('form-wifi'), - email: document.getElementById('form-email') - }; + // Inputs + const modeSelector = document.getElementById('mode-selector'); + const optEcc = document.getElementById('opt-ecc'); + const optSize = document.getElementById('opt-size'); + const optFg = document.getElementById('opt-fg'); + const optBg = document.getElementById('opt-bg'); + + // Hex Labels (for display) + const hexFg = document.getElementById('hex-fg'); + const hexBg = document.getElementById('hex-bg'); - // Inputs collection for event binding - const allInputs = document.querySelectorAll('input, textarea, select'); - - // Initialize QR Library - // Using CorrectLevel.H (High) as requested - let qrcode = new QRCode(qrContainer, { - width: 256, - height: 256, - colorDark : "#000000", - colorLight : "#ffffff", - correctLevel : QRCode.CorrectLevel.H - }); - - // 2. State & Logic - let currentMode = 'text'; + let qrcodeObj = null; let debounceTimer; - // Switch Input Forms - function switchMode(newMode) { - currentMode = newMode; - - // Hide all forms - Object.values(forms).forEach(form => form.classList.remove('active')); - - // Show selected form - forms[newMode].classList.add('active'); + // --- LOGIC --- - // Trigger update immediately + function createQRInstance() { + qrContainer.innerHTML = ''; + + // Validation: Clamp size to prevent crashing browser + let size = parseInt(optSize.value); + if (isNaN(size) || size < 64) size = 64; + if (size > 4000) size = 4000; + + const ecc = QRCode.CorrectLevel[optEcc.value]; + const colorDark = optFg.value; + const colorLight = optBg.value; + + // Update Hex Labels + hexFg.textContent = colorDark; + hexBg.textContent = colorLight; + + qrcodeObj = new QRCode(qrContainer, { + width: size, + height: size, + colorDark : colorDark, + colorLight : colorLight, + correctLevel : ecc + }); + updateQR(); } - // String Builders function getDataString() { - if (currentMode === 'text') { + const mode = modeSelector.value; + if (mode === 'text') { return document.getElementById('inp-text').value; - } - - else if (currentMode === 'wifi') { + } else if (mode === 'wifi') { const ssid = document.getElementById('inp-wifi-ssid').value; const pass = document.getElementById('inp-wifi-pass').value; const type = document.getElementById('inp-wifi-type').value; - - if (!ssid) return ''; // SSID is minimum requirement - - // Format: WIFI:S:MyNetwork;T:WPA;P:mypassword;; - // Note: Special characters in SSID/Pass should ideally be escaped, - // but standard readers handle raw strings well usually. - // Adding escape for semicolons/colons is safer practice: + if (!ssid) return ''; const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1'); const cleanPass = pass.replace(/([\\;,:])/g, '\\$1'); - return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`; - } - - else if (currentMode === 'email') { + } else if (mode === 'email') { const to = document.getElementById('inp-email-to').value; const sub = document.getElementById('inp-email-sub').value; const body = document.getElementById('inp-email-body').value; - if (!to) return ''; - return `mailto:${to}?subject=${encodeURIComponent(sub)}&body=${encodeURIComponent(body)}`; } - return ''; } - // QR Update Logic function updateQR() { + if (!qrcodeObj) return; const data = getDataString(); if (!data || data.trim() === '') { - qrcode.clear(); // Clear the code + qrcodeObj.clear(); downloadBtn.disabled = true; return; } downloadBtn.disabled = false; - - // Visual Stability: Fade opacity slightly during update logic if desired, - // but qrcode.makeCode is instantaneous on small data. - // We ensure high performance by not recreating the object. - qrcode.makeCode(data); + qrcodeObj.makeCode(data); } - // Debounce Function - function handleInput() { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => { - updateQR(); - }, 300); // 300ms delay - } + // --- EVENT LISTENERS --- - // 3. Event Listeners - - // Mode Switching - modeSelector.addEventListener('change', (e) => switchMode(e.target.value)); - - // Input Detection (Auto-Update) - allInputs.forEach(input => { - // Skip mode selector as it has its own handler - if(input.id !== 'mode-selector') { - input.addEventListener('input', handleInput); - } + // 1. Config Changes (Re-create instance) + // We listen to 'change' for colors (happens when picker closes) + // and 'input' (happens while dragging) depending on preference. + // 'input' is smoother but more CPU intensive. Let's use 'input'. + [optEcc, optSize, optFg, optBg].forEach(el => { + el.addEventListener('input', () => { + // Debounce the recreation slightly to prevent lag during color dragging + clearTimeout(debounceTimer); + debounceTimer = setTimeout(createQRInstance, 100); + }); }); - // Download Logic + // 2. Mode Switching + modeSelector.addEventListener('change', (e) => { + const newMode = e.target.value; + document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active')); + document.getElementById(`form-${newMode}`).classList.add('active'); + updateQR(); + }); + + // 3. Data Entry (Text, WiFi, Email) + document.querySelectorAll('.input-form').forEach(form => { + form.addEventListener('input', () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(updateQR, 300); + }); + }); + + // 4. Download downloadBtn.addEventListener('click', () => { - // Find the image generated by qrcode.js const img = qrContainer.querySelector('img'); - if (img && img.src) { const link = document.createElement('a'); link.href = img.src; - link.download = `qrcode-${currentMode}-${Date.now()}.png`; + link.download = `qrcode-${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }); - // Initial run - switchMode('text'); + // Init + createQRInstance(); }); diff --git a/specs/geminini.md b/specs/geminini.md index bed3aae..80f4f02 100644 --- a/specs/geminini.md +++ b/specs/geminini.md @@ -1,7 +1,7 @@ -### Specification v0.2.0: Multi-Format Static QR Generator +### Specification v0.2.1: Multi-Format Static QR Generator **1. Project Overview** -Build a fast, offline-capable single-page application that generates QR codes for various data types (URL, WiFi, Email). The application must feature a responsive split-pane layout and polished, non-jarring visual updates. +Build a fast, offline-capable single-page application that generates QR codes. The application must feature a high-contrast, utilitarian interface that prioritizes raw functionality and visual hierarchy over modern softness. **2. Technical Constraints** @@ -13,57 +13,56 @@ Build a fast, offline-capable single-page application that generates QR codes fo **3. Functional Requirements** * **Input Modes:** -* The interface must support different input forms based on the selected mode: 1. **URL / Text (Default):** Single Textarea. -2. **WiFi:** Inputs for "SSID" (Network Name), "Password", and "Encryption Type" (WPA/WEP/None). The JS must format this into the standard WiFi string format (e.g., `WIFI:S:MyNetwork;T:WPA;P:mypassword;;`). -3. **Email:** Inputs for "To", "Subject", and "Body". - - - - -* **Generation Logic:** -* **Auto-Update:** The QR code updates automatically as the user types (with debouncing). -* **Error Correction:** High (H). - - -* **Download:** -* Button text: "Download". -* Format: PNG. +2. **WiFi:** Inputs for SSID, Password, Encryption. Formats to `WIFI:S:...`. +3. **Email:** Inputs for To, Subject, Body. Formats to `mailto:...`. +* **Generation Logic:** Auto-update with debouncing. High (H) error correction. +* **Output:** PNG Download. **4. UI/UX & Layout Design** -* **Grid Layout (The "Side-by-Side" Requirement):** -* **Desktop/Tablet:** A two-column layout. -* **Left Pane:** Input controls (Mode selector + Input fields). -* **Right Pane:** The generated QR code + Download button. -* *Vertical alignment:* The QR code should remain sticky or centered vertically as the input form grows. +* **Desktop Layout (The "Broadsheet" View):** +* **Split Pane:** Fixed-width sidebar (Left) for controls; Fluid container (Right) for output. +* **Visual Texture:** The Right Pane should feature a subtle "engineering paper" dot-grid background to distinguish the workspace from the controls. +* **Sticky Preview:** The QR code container must remain fixed in view while scrolling through long input forms. -* **Mobile:** Automatically stack vertically (Inputs on top, QR on bottom) using CSS Media Queries. - - -* **Visual Stability (The "Anti-Flicker" Requirement):** -* **Fixed Dimensions:** The QR code container must have a reserved, fixed aspect ratio/size to prevent the rest of the page from "jumping" or reflowing when the QR code redraws. -* **Subtle Transition:** -* Use a CSS `transition` on the QR code container (specifically `opacity` or `filter`). -* When the code updates, it should not "flash" white. A standard approach is a very fast (e.g., 200ms) cross-fade or simply keeping the opacity at 1 and letting the canvas repaint instantly. -* *Developer Note:* Avoid destroying and recreating the DOM element if possible; update the canvas context or `src` attribute smoothly. +* **Mobile Layout:** Stacked vertically. Top border acts as a "Masthead." +* **Interaction Design:** +* **Anti-Flicker:** The QR container preserves its dimensions to prevent layout shifts. +* **Feedback:** No soft transitions. State changes (like button hovers) should be sharp and instantaneous or high-contrast (e.g., inverting colors). +**5. Styling Guidelines (The "Brutalist" Update)** + +* **Design Philosophy:** "Editorial Brutalism" / "1990s Industrial." +* *Reference:* New York Times structure meets NeXTSTEP utility. +* *Core Rule:* **No rounded corners (`border-radius: 0`).** -**5. Styling Guidelines** +* **Color Palette:** +* **Strict Monochrome:** Pure Black (`#000`) and White (`#fff`). +* **Contrast:** High contrast borders (1px solid black) define all containment areas. + + +* **Typography (The "Data vs. UI" Split):** +* **Interface/Labels:** Serif (Georgia, Times New Roman). Represents the "System" or "Authority." +* **User Inputs:** Monospace (Courier, Lucida Console). Represents "Raw Data." + + +* **Visual Elements:** +* **Hard Shadows:** Elements meant to "pop" (like the QR container) use hard, solid-color block shadows (e.g., `10px 10px 0px #000`) rather than soft blurs. +* **Borders:** All inputs and buttons have visible, consistent 1px solid borders. +* **Buttons:** Uppercase, Monospace, solid black background. Inverts to white background/black text on hover. + -* **Aesthetic:** Clean, professional, minimal. -* **Colors:** Neutral tones. The focus should be on the functionality. -* **Typography:** System fonts. **6. Deliverables** -* `index.html` (Semantic markup with input forms for all 3 modes). -* `style.css` (Flexbox/Grid for layout + transitions). -* `script.js` (Logic for string formatting WiFi/Email and QR generation). +* `index.html` (Semantic markup). +* `style.css` (Strict Grid/Flexbox with brutalist variables). +* `script.js` (Logic). * `README.md` diff --git a/style.css b/style.css index c408e8d..b07c706 100644 --- a/style.css +++ b/style.css @@ -1,13 +1,15 @@ - :root { + /* Color Palette - Strict Monochrome */ --bg-color: #ffffff; --text-color: #000000; --border-color: #000000; - --accent-color: #000000; - --input-bg: #ffffff; + --pane-bg: #ffffff; /* Typography */ - --font-serif: "Georgia", "Times New Roman", Times, serif; + /* Serif for the Interface/Authority */ + /* --font-serif: "Georgia", "Times New Roman", Times, serif; */ + --font-serif: "Courier New", Courier, "Lucida Sans Typewriter", monospace; + /* Monospace for the Data/Inputs */ --font-mono: "Courier New", Courier, "Lucida Sans Typewriter", monospace; } @@ -26,20 +28,25 @@ body { -webkit-font-smoothing: antialiased; } -/* Layout */ +/* LAYOUT: CSS GRID + Rows: Content (1fr) -> Footer (auto) +*/ .app-container { display: grid; grid-template-columns: 1fr; + grid-template-rows: 1fr auto; min-height: 100vh; - border-top: 5px solid var(--border-color); /* The "Masthead" thick line */ + border-top: 5px solid var(--border-color); /* Masthead line */ } .pane { padding: 2rem 2.5rem; position: relative; + background: var(--pane-bg); } -/* Header */ +/* --- HEADER & TYPOGRAPHY --- */ + header h1 { font-size: 2.5rem; font-weight: 700; @@ -57,12 +64,8 @@ hr { margin: 2rem 0; } -/* Inputs Pane */ -.input-pane { - background: var(--bg-color); -} +/* --- INPUTS PANE (Left) --- */ -/* Controls */ .control-group { margin-bottom: 1.5rem; } @@ -76,20 +79,20 @@ label { letter-spacing: 0.05em; } -/* The "Unorthodox" Input Style */ +/* The Brutalist Input Style */ input, textarea, select { width: 100%; padding: 1rem; background-color: transparent; border: 1px solid var(--border-color); - border-radius: 0; /* Crucial */ + border-radius: 0; /* Crucial: No rounded corners */ - /* Typography mix */ + /* Data font */ font-family: var(--font-mono); font-size: 1rem; color: var(--text-color); - /* Remove native OS styles */ + /* Reset native styles */ appearance: none; -webkit-appearance: none; box-shadow: none; @@ -101,7 +104,7 @@ input:focus, textarea:focus, select:focus { background-color: #fff; } -/* Custom Dropdown Arrow for the Brutalist look */ +/* Custom Arrow for Select */ select { background-image: url('data:image/svg+xml;utf8,'); background-repeat: no-repeat; @@ -110,11 +113,51 @@ select { cursor: pointer; } +/* Form Visibility Animation */ .input-form { display: none; animation: fadeIn 0.3s ease; } +/* Advanced Options Module */ +details.advanced-options { + margin-top: 2rem; + border: 1px solid var(--border-color); + padding: 0; + background: #fdfdfd; /* Slight contrast from white */ +} + +summary { + cursor: pointer; + padding: 1rem; + font-family: var(--font-mono); + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + list-style: none; /* Hide default triangle */ + background-color: #f0f0f0; + border-bottom: 1px solid transparent; /* Prepare for expansion */ + transition: background 0.1s; +} + +/* Custom indicator logic if you want complete control, + but standard text [+]/[-] is very brutalist. */ +details[open] summary { + border-bottom: 1px solid var(--border-color); + background-color: var(--border-color); /* Invert header when open */ + color: var(--bg-color); +} + +details .control-group { + padding: 1.5rem; + margin-bottom: 0; /* Override default margin inside box */ + border-bottom: 1px solid var(--border-color); +} + +details .control-group:last-child { + border-bottom: none; +} + .input-form.active { display: block; } @@ -124,14 +167,13 @@ select { to { opacity: 1; transform: translateY(0); } } -/* Preview Pane */ +/* --- PREVIEW PANE (Right) --- */ + .preview-pane { display: flex; flex-direction: column; align-items: center; - /* Create a texture or distinct feel for the right side? - Let's keep it stark white but separated by a border. */ - background-color: var(--bg-color); + justify-content: flex-start; } .sticky-wrapper { @@ -140,31 +182,28 @@ select { padding-top: 1rem; } -/* QR Container */ +/* QR Container - The "Art" */ #qr-container { width: 256px; height: 256px; margin: 0 auto 2rem auto; - /* The "Frame" */ + /* Frame */ border: 1px solid var(--border-color); - padding: 10px; /* Mat frame */ + padding: 10px; display: flex; align-items: center; justify-content: center; - - /* Hard transition, no fading */ - transition: none; + background: #fff; } #qr-container img, #qr-container canvas { display: block; max-width: 100%; - /* Ensure crisp edges */ - image-rendering: pixelated; + image-rendering: pixelated; /* Crisp edges */ } -/* The Button - Brutalist */ +/* Buttons */ button { width: 100%; padding: 1rem; @@ -184,7 +223,7 @@ button { button:hover:not(:disabled) { background-color: var(--bg-color); color: var(--text-color); - /* The border remains black, creating an "invert" effect */ + /* Inverted effect */ } button:disabled { @@ -192,40 +231,194 @@ button:disabled { color: #aaa; border-color: #ccc; cursor: not-allowed; - text-decoration: line-through; /* A little edgy touch */ + text-decoration: line-through; } -/* Desktop Grid */ +/* --- FOOTER --- */ + +.app-footer { + grid-column: 1 / -1; /* Span full width */ + border-top: 5px solid var(--border-color); /* Thick closure line */ + padding: 3rem 2rem; + background-color: var(--bg-color); + text-align: center; +} + +.footer-content { + max-width: 600px; + margin: 0 auto; + font-size: 0.8rem; + color: var(--text-color); + opacity: 0.8; +} + +.app-footer p { + margin-bottom: 0.5rem; +} + +.app-footer a { + color: var(--text-color); + text-decoration: underline; + font-family: var(--font-mono); +} + +.app-footer a:hover { + background-color: var(--text-color); + color: var(--bg-color); + text-decoration: none; +} + +/* --- RESPONSIVE / DESKTOP --- */ + @media (min-width: 768px) { .app-container { - grid-template-columns: 400px 1fr; /* Fixed width sidebar, fluid content */ + /* Sidebar fixed 400px, Content fluid */ + grid-template-columns: 400px 1fr; max-width: 100%; margin: 0; - border-top: none; /* Remove top border on desktop */ + border-top: none; } .input-pane { - border-right: 1px solid var(--border-color); /* Vertical divider */ - height: 100vh; - overflow-y: auto; + border-right: 1px solid var(--border-color); + /* Allow natural height so page scrolls to footer */ + height: auto; + min-height: 100vh; } .preview-pane { - height: 100vh; - justify-content: center; + min-height: 100vh; + justify-content: center; /* Center Vertically */ + + /* Engineering Paper Texture */ + background-color: #fdfdfd; background-image: radial-gradient(#000 0.5px, transparent 0.5px); - background-size: 20px 20px; /* Subtle dot grid pattern on the right */ + background-size: 20px 20px; } - /* Make the pattern very subtle */ - .preview-pane { - background-color: #fdfdfd; - } - .sticky-wrapper { + position: sticky; + top: 2rem; background: #fff; padding: 2rem; border: 1px solid black; - box-shadow: 10px 10px 0px 0px rgba(0,0,0,1); /* Hard block shadow */ + box-shadow: 10px 10px 0px 0px #000; /* Hard Block Shadow */ } } + +/* --- PRINT STYLES (The Manifest) --- */ + +@media print { + /* Hide Interface */ + .input-pane, header, hr, button, .app-footer { + display: none !important; + } + + .app-container { + display: block; + border: none; + margin: 0; + padding: 0; + } + + .preview-pane { + height: auto; + min-height: 0; + background: none; + display: block; + padding: 0; + } + + .sticky-wrapper { + border: 4px solid black; + box-shadow: none; + max-width: 100%; + width: 100%; + padding: 4rem; + position: static; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + #qr-container { + border: none; + width: 400px; + height: 400px; + margin-bottom: 2rem; + } + + /* Print Timestamp/Watermark */ + .sticky-wrapper::after { + content: "GENERATED OUTPUT // DO NOT FOLD"; + display: block; + margin-top: 2rem; + font-family: var(--font-mono); + font-size: 1rem; + font-weight: bold; + letter-spacing: 2px; + text-transform: uppercase; + border-top: 2px solid black; + padding-top: 1rem; + width: 100%; + text-align: center; + } +} + +/* Styling the Number Input */ +input[type="number"] { + -moz-appearance: textfield; /* Remove Firefox spinner */ +} +/* Remove Webkit spinners if you prefer a cleaner look */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Color Group Layout */ +.color-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.color-input label { + margin-bottom: 0.5rem; +} + +.input-wrapper { + display: flex; + align-items: center; + border: 1px solid var(--border-color); + padding: 0.5rem; + background: #fff; +} + +/* The Color Swatch Itself */ +input[type="color"] { + border: none; + width: 2rem; + height: 2rem; + padding: 0; + margin-right: 1rem; + background: none; + cursor: pointer; +} + +/* Color Swatch Internal (Webkit) */ +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} +input[type="color"]::-webkit-color-swatch { + border: 1px solid var(--border-color); + border-radius: 0; /* Square swatch */ +} + +/* The Hex Code Text next to it */ +.input-wrapper span { + font-family: var(--font-mono); + font-size: 0.9rem; + text-transform: uppercase; +}