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
+
+
+
+
+
@@ -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;
+}