Make some big updates
This commit is contained in:
5
favicon.svg
Normal file
5
favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 7" shape-rendering="crispEdges">
|
||||
<rect x="0" y="0" width="7" height="7" fill="black"/>
|
||||
<rect x="1" y="1" width="5" height="5" fill="white"/>
|
||||
<rect x="2" y="2" width="3" height="3" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 263 B |
59
index.html
59
index.html
@@ -3,10 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QR Generator</title>
|
||||
<title>Simple QR Generator</title>
|
||||
<meta name="description" content="Fast, offline, multi-format QR code generator.">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" crossorigin="anonymous" referrerpolicy="no-referrer" defer></script>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<script src="qrcode.min.js"></script>
|
||||
<script src="script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -68,6 +69,46 @@
|
||||
<textarea id="inp-email-body" rows="4" placeholder="Message content..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="advanced-options">
|
||||
<summary>ADVANCED CONFIGURATION</summary>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="opt-ecc">Error Correction</label>
|
||||
<select id="opt-ecc">
|
||||
<option value="H" selected>High (30%) - Best for Print</option>
|
||||
<option value="Q">Quartile (25%)</option>
|
||||
<option value="M">Medium (15%)</option>
|
||||
<option value="L">Low (7%) - Highest Capacity</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="opt-size">Resolution (px)</label>
|
||||
<input type="number" id="opt-size" value="256" min="64" max="4000" step="32">
|
||||
<small style="display:block; margin-top:0.5rem; font-family:var(--font-mono); opacity:0.6;">
|
||||
Min: 64px // Max: 4000px
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="control-group color-group">
|
||||
<div class="color-input">
|
||||
<label for="opt-fg">Foreground</label>
|
||||
<div class="input-wrapper">
|
||||
<input type="color" id="opt-fg" value="#000000">
|
||||
<span id="hex-fg">#000000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-input">
|
||||
<label for="opt-bg">Background</label>
|
||||
<div class="input-wrapper">
|
||||
<input type="color" id="opt-bg" value="#ffffff">
|
||||
<span id="hex-bg">#FFFFFF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="pane preview-pane">
|
||||
@@ -77,6 +118,20 @@
|
||||
<button id="btn-download" disabled>Download PNG</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<p>
|
||||
© 2025 figtree
|
||||
</p>
|
||||
<p>
|
||||
Source code licensed under <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPLv3</a>.
|
||||
</p>
|
||||
<p>
|
||||
Powered by <a href="https://davidshimjs.github.io/qrcodejs/" target="_blank">qrcode.js</a> (MIT).
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
159
script.js
159
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();
|
||||
});
|
||||
|
||||
@@ -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`
|
||||
|
||||
287
style.css
287
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,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"><path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z"/></svg>');
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user