Compare commits
20 Commits
main
...
capacityba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d20b58547 | ||
|
|
875e29841f | ||
|
|
5c674a2f3d | ||
|
|
91b0e13478 | ||
|
|
693ebf0172 | ||
|
|
7db5c18018 | ||
|
|
76c348cb97 | ||
|
|
eb1e5a7fcd | ||
|
|
b6dd1392a2 | ||
|
|
cc09ecf474 | ||
|
|
30d86b1fce | ||
|
|
140287db51 | ||
|
|
5b8583cf85 | ||
|
|
e20ae120af | ||
|
|
205876a28f | ||
|
|
6ef65c87a0 | ||
|
|
4da714e4b9 | ||
|
|
479e22e151 | ||
|
|
446b3b46fd | ||
|
|
c3fdf45015 |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 22 KiB |
46
index.html
46
index.html
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Simple QR Generator</title>
|
<title>QRDAMAGE.NET</title>
|
||||||
<meta name="description" content="Fast, offline, multi-format QR code generator.">
|
<meta name="description" content="Fast, offline, multi-format QR code generator.">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<main class="app-container">
|
<main class="app-container">
|
||||||
<section class="pane input-pane">
|
<section class="pane input-pane">
|
||||||
<header>
|
<header>
|
||||||
<h1>QR Generator</h1>
|
<h1>QR GENERATOR</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
@@ -70,9 +70,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="capacity-meter" style="display:none; margin-top:0.75rem; width:100%;">
|
||||||
|
<div style="display:flex; justify-content:space-between; font-size:0.7rem; font-family:var(--font-mono); margin-bottom:0.2rem;">
|
||||||
|
<span id="capacity-label">CAPACITY</span>
|
||||||
|
<span id="capacity-text">0%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="capacity-track">
|
||||||
|
<div id="capacity-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<details class="advanced-options">
|
<details class="advanced-options">
|
||||||
<summary>ADVANCED CONFIGURATION</summary>
|
<summary>ADVANCED CONFIGURATION</summary>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="opt-ecc">Error Correction</label>
|
<label for="opt-ecc">Error Correction</label>
|
||||||
<select id="opt-ecc">
|
<select id="opt-ecc">
|
||||||
@@ -99,7 +110,7 @@
|
|||||||
<span id="hex-fg">#000000</span>
|
<span id="hex-fg">#000000</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="color-input">
|
<div class="color-input">
|
||||||
<label for="opt-bg">Background</label>
|
<label for="opt-bg">Background</label>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
@@ -108,14 +119,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Output Format</label>
|
||||||
|
<div class="format-selector">
|
||||||
|
<label style="margin:0; display:flex; align-items:center; cursor:pointer;">
|
||||||
|
<input type="radio" name="opt-fmt" value="png" checked style="width:auto; margin-right:0.5rem;">
|
||||||
|
PNG
|
||||||
|
</label>
|
||||||
|
<label style="margin:0; display:flex; align-items:center; cursor:pointer;">
|
||||||
|
<input type="radio" name="opt-fmt" value="svg" style="width:auto; margin-right:0.5rem;">
|
||||||
|
SVG
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<button id="btn-clear">
|
||||||
|
RESET
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pane preview-pane">
|
<section class="pane preview-pane">
|
||||||
<div class="sticky-wrapper">
|
<div class="sticky-wrapper">
|
||||||
<div id="qr-container">
|
<div style="display:flex; justify-content:space-between; align-items:flex-end; margin-bottom:0.5rem;">
|
||||||
</div>
|
<span style="font-family:var(--font-mono); font-weight:700; font-size:0.8rem;">PREVIEW</span>
|
||||||
<button id="btn-download" disabled>Download PNG</button>
|
<span id="resolution-display" style="font-family:var(--font-mono); font-size:0.7rem; opacity:0.6;">256 x 256 px</span>
|
||||||
|
</div>
|
||||||
|
<div id="qr-container"> </div>
|
||||||
|
|
||||||
|
<button id="btn-download" disabled>DOWNLOAD PNG</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
370
script.js
370
script.js
@@ -1,49 +1,46 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// --- GLOBALS ---
|
// --- 0. CONSTANTS ---
|
||||||
|
// Limits based on Version 40 (Byte Mode)
|
||||||
|
const MAX_CAPACITY = {
|
||||||
|
'L': 2950,
|
||||||
|
'M': 2328,
|
||||||
|
'Q': 1660,
|
||||||
|
'H': 1270
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 1. DOM REFERENCES ---
|
||||||
const qrContainer = document.getElementById('qr-container');
|
const qrContainer = document.getElementById('qr-container');
|
||||||
const downloadBtn = document.getElementById('btn-download');
|
const downloadBtn = document.getElementById('btn-download');
|
||||||
|
const stickyWrapper = document.querySelector('.sticky-wrapper');
|
||||||
|
const clearBtn = document.getElementById('btn-clear');
|
||||||
|
|
||||||
|
// Gauge Elements
|
||||||
|
const capMeter = document.getElementById('capacity-meter');
|
||||||
|
const capBar = document.getElementById('capacity-bar');
|
||||||
|
const capText = document.getElementById('capacity-text');
|
||||||
|
const capLabel = document.getElementById('capacity-label');
|
||||||
|
|
||||||
// Inputs
|
// Inputs
|
||||||
const modeSelector = document.getElementById('mode-selector');
|
const modeSelector = document.getElementById('mode-selector');
|
||||||
const optEcc = document.getElementById('opt-ecc');
|
const optEcc = document.getElementById('opt-ecc');
|
||||||
const optSize = document.getElementById('opt-size');
|
const optSize = document.getElementById('opt-size');
|
||||||
const optFg = document.getElementById('opt-fg');
|
const optFg = document.getElementById('opt-fg');
|
||||||
const optBg = document.getElementById('opt-bg');
|
const optBg = document.getElementById('opt-bg');
|
||||||
|
|
||||||
// Hex Labels (for display)
|
|
||||||
const hexFg = document.getElementById('hex-fg');
|
const hexFg = document.getElementById('hex-fg');
|
||||||
const hexBg = document.getElementById('hex-bg');
|
const hexBg = document.getElementById('hex-bg');
|
||||||
|
|
||||||
let qrcodeObj = null;
|
const fmtRadios = document.getElementsByName('opt-fmt');
|
||||||
|
|
||||||
let debounceTimer;
|
let debounceTimer;
|
||||||
|
let renderSeq = 0;
|
||||||
|
|
||||||
// --- LOGIC ---
|
// --- 2. STATE GETTERS ---
|
||||||
|
|
||||||
function createQRInstance() {
|
function getFormat() {
|
||||||
qrContainer.innerHTML = '';
|
for (const r of fmtRadios) {
|
||||||
|
if (r.checked) return r.value;
|
||||||
// Validation: Clamp size to prevent crashing browser
|
}
|
||||||
let size = parseInt(optSize.value);
|
return 'png';
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDataString() {
|
function getDataString() {
|
||||||
@@ -54,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const ssid = document.getElementById('inp-wifi-ssid').value;
|
const ssid = document.getElementById('inp-wifi-ssid').value;
|
||||||
const pass = document.getElementById('inp-wifi-pass').value;
|
const pass = document.getElementById('inp-wifi-pass').value;
|
||||||
const type = document.getElementById('inp-wifi-type').value;
|
const type = document.getElementById('inp-wifi-type').value;
|
||||||
if (!ssid) return '';
|
if (!ssid) return '';
|
||||||
const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1');
|
const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1');
|
||||||
const cleanPass = pass.replace(/([\\;,:])/g, '\\$1');
|
const cleanPass = pass.replace(/([\\;,:])/g, '\\$1');
|
||||||
return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`;
|
return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`;
|
||||||
@@ -68,63 +65,298 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateQR() {
|
// --- 3. RENDER HELPERS ---
|
||||||
if (!qrcodeObj) return;
|
|
||||||
const data = getDataString();
|
function generateSVGString(modules, size, fg, bg) {
|
||||||
|
const count = modules.length;
|
||||||
if (!data || data.trim() === '') {
|
const modSize = size / count;
|
||||||
qrcodeObj.clear();
|
let pathData = '';
|
||||||
downloadBtn.disabled = true;
|
for (let r = 0; r < count; r++) {
|
||||||
|
for (let c = 0; c < count; c++) {
|
||||||
|
if (modules[r][c]) {
|
||||||
|
const x = c * modSize;
|
||||||
|
const y = r * modSize;
|
||||||
|
pathData += `M${x},${y}h${modSize}v${modSize}h-${modSize}z`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
|
||||||
|
<rect width="100%" height="100%" fill="${bg}"/>
|
||||||
|
<path d="${pathData}" fill="${fg}" shape-rendering="crispEdges"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderError(title, subtitle) {
|
||||||
|
qrContainer.innerHTML = `
|
||||||
|
<div class="qr-error-msg">
|
||||||
|
ERROR: ${title}<br>
|
||||||
|
<span style="font-size:0.7em; opacity:0.8; display:block; margin-top:0.5rem;">
|
||||||
|
${subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
downloadBtn.disabled = true;
|
||||||
|
downloadBtn.textContent = "GENERATION FAILED";
|
||||||
|
stickyWrapper.classList.add('has-error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCapacityUI(textData) {
|
||||||
|
if (!textData) {
|
||||||
|
capMeter.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = new Blob([textData]);
|
||||||
|
const bytes = blob.size;
|
||||||
|
|
||||||
|
const maxBytes = MAX_CAPACITY[optEcc.value] || 1270;
|
||||||
|
|
||||||
|
const rawUsage = Math.round((bytes / maxBytes) * 100);
|
||||||
|
const usage = Math.min(100, rawUsage);
|
||||||
|
|
||||||
|
if (usage > 50) {
|
||||||
|
capMeter.style.display = 'block';
|
||||||
|
capBar.style.width = `${usage}%`;
|
||||||
|
|
||||||
|
capText.textContent = `${usage}% (${bytes} / ${maxBytes} B)`;
|
||||||
|
|
||||||
|
if (bytes <= maxBytes) {
|
||||||
|
capLabel.textContent = 'CAPACITY';
|
||||||
|
} else {
|
||||||
|
capLabel.textContent = 'OVER CAPACITY';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usage > 90) {
|
||||||
|
// Critical Level: Force RED
|
||||||
|
capBar.style.backgroundColor = 'red';
|
||||||
|
capText.style.color = 'red';
|
||||||
|
capLabel.style.color = 'red';
|
||||||
|
} else {
|
||||||
|
// Normal Level: Let CSS variables handle it (Black/White)
|
||||||
|
capBar.style.backgroundColor = '';
|
||||||
|
capText.style.color = '';
|
||||||
|
capLabel.style.color = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
capMeter.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. CORE RENDERER ---
|
||||||
|
|
||||||
|
function renderQR() {
|
||||||
|
const mySeq = ++renderSeq;
|
||||||
|
|
||||||
|
// 1. Gather State
|
||||||
|
const textData = getDataString();
|
||||||
|
const format = getFormat();
|
||||||
|
const ecc = QRCode.CorrectLevel[optEcc.value];
|
||||||
|
const colorDark = optFg.value;
|
||||||
|
const colorLight = optBg.value;
|
||||||
|
|
||||||
|
// 2. Validate Size & Labels
|
||||||
|
let size = parseInt(optSize.value) || 256;
|
||||||
|
if (size < 64) size = 64;
|
||||||
|
if (size > 4000) size = 4000;
|
||||||
|
|
||||||
|
hexFg.textContent = colorDark;
|
||||||
|
hexBg.textContent = colorLight;
|
||||||
|
|
||||||
|
const resDisplay = document.getElementById('resolution-display');
|
||||||
|
if (resDisplay) resDisplay.textContent = `${size} x ${size} px`;
|
||||||
|
|
||||||
|
// 3. Reset Error State
|
||||||
|
stickyWrapper.classList.remove('has-error');
|
||||||
|
|
||||||
|
// 4. Handle Empty State
|
||||||
|
if (!textData || textData.trim() === '') {
|
||||||
|
if (mySeq === renderSeq) {
|
||||||
|
qrContainer.innerHTML = '';
|
||||||
|
downloadBtn.disabled = true;
|
||||||
|
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadBtn.disabled = false;
|
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
|
||||||
qrcodeObj.makeCode(data);
|
|
||||||
|
// 5. Generate (Off-screen)
|
||||||
|
try {
|
||||||
|
const tempContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const instance = new QRCode(tempContainer, {
|
||||||
|
text: textData,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
colorDark : colorDark,
|
||||||
|
colorLight : colorLight,
|
||||||
|
correctLevel : ecc
|
||||||
|
});
|
||||||
|
|
||||||
|
if (format === 'svg') {
|
||||||
|
const modules = instance._oQRCode.modules;
|
||||||
|
const svgString = generateSVGString(modules, size, colorDark, colorLight);
|
||||||
|
|
||||||
|
if (mySeq === renderSeq) {
|
||||||
|
qrContainer.innerHTML = svgString;
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// PNG Mode
|
||||||
|
const canvas = tempContainer.querySelector('canvas');
|
||||||
|
if (canvas) {
|
||||||
|
const dataUrl = canvas.toDataURL("image/png");
|
||||||
|
const newImg = new Image();
|
||||||
|
|
||||||
|
newImg.style.display = 'block';
|
||||||
|
newImg.style.width = '100%';
|
||||||
|
newImg.style.height = '100%';
|
||||||
|
newImg.style.imageRendering = 'pixelated';
|
||||||
|
newImg.src = dataUrl;
|
||||||
|
|
||||||
|
const ready = newImg.decode
|
||||||
|
? newImg.decode()
|
||||||
|
: new Promise((resolve, reject) => {
|
||||||
|
newImg.onload = resolve;
|
||||||
|
newImg.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
ready.then(() => {
|
||||||
|
if (mySeq !== renderSeq) return;
|
||||||
|
qrContainer.replaceChildren(newImg);
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
}).catch(() => {
|
||||||
|
if (mySeq !== renderSeq) return;
|
||||||
|
qrContainer.replaceChildren(newImg);
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (mySeq !== renderSeq) return;
|
||||||
|
|
||||||
|
const blob = new Blob([textData]);
|
||||||
|
const bytes = blob.size;
|
||||||
|
|
||||||
|
const maxBytes = MAX_CAPACITY[optEcc.value] || 1270;
|
||||||
|
|
||||||
|
if (bytes >= maxBytes) {
|
||||||
|
renderError("CAPACITY EXCEEDED", "REDUCE TEXT OR LOWER ECC LEVEL");
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
renderError("UNKNOWN ERROR", `CODE: ${e.name || 'Except'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- EVENT LISTENERS ---
|
function handleUpdate(immediate = false) {
|
||||||
|
const text = getDataString();
|
||||||
|
updateCapacityUI(text);
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
if (immediate) {
|
||||||
|
renderQR();
|
||||||
|
} else {
|
||||||
|
debounceTimer = setTimeout(renderQR, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Config Changes (Re-create instance)
|
// --- 5. RESET LOGIC ---
|
||||||
// We listen to 'change' for colors (happens when picker closes)
|
function resetApp() {
|
||||||
// and 'input' (happens while dragging) depending on preference.
|
const inputs = document.querySelectorAll('.input-form input, .input-form textarea');
|
||||||
// 'input' is smoother but more CPU intensive. Let's use 'input'.
|
inputs.forEach(el => el.value = '');
|
||||||
|
|
||||||
|
modeSelector.value = 'text';
|
||||||
|
|
||||||
|
optEcc.value = 'H';
|
||||||
|
optSize.value = '256';
|
||||||
|
|
||||||
|
optFg.value = '#000000';
|
||||||
|
hexFg.textContent = '#000000';
|
||||||
|
|
||||||
|
optBg.value = '#ffffff';
|
||||||
|
hexBg.textContent = '#FFFFFF';
|
||||||
|
|
||||||
|
for (const r of fmtRadios) {
|
||||||
|
if (r.value === 'png') r.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
|
||||||
|
document.getElementById('form-text').classList.add('active');
|
||||||
|
|
||||||
|
handleUpdate(true);
|
||||||
|
|
||||||
|
const firstField = document.querySelector('#form-text textarea');
|
||||||
|
if (firstField) firstField.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 6. LISTENERS ---
|
||||||
|
// (Listeners remain the same)
|
||||||
[optEcc, optSize, optFg, optBg].forEach(el => {
|
[optEcc, optSize, optFg, optBg].forEach(el => {
|
||||||
el.addEventListener('input', () => {
|
el.addEventListener('input', () => handleUpdate(false));
|
||||||
// Debounce the recreation slightly to prevent lag during color dragging
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
debounceTimer = setTimeout(createQRInstance, 100);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Mode Switching
|
fmtRadios.forEach(r => r.addEventListener('change', () => handleUpdate(true)));
|
||||||
|
|
||||||
modeSelector.addEventListener('change', (e) => {
|
modeSelector.addEventListener('change', (e) => {
|
||||||
const newMode = e.target.value;
|
const newMode = e.target.value;
|
||||||
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
|
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
|
||||||
document.getElementById(`form-${newMode}`).classList.add('active');
|
document.getElementById(`form-${newMode}`).classList.add('active');
|
||||||
updateQR();
|
|
||||||
|
const newForm = document.getElementById(`form-${newMode}`);
|
||||||
|
const firstField = newForm.querySelector('input, textarea');
|
||||||
|
if (firstField) firstField.focus();
|
||||||
|
|
||||||
|
handleUpdate(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Data Entry (Text, WiFi, Email)
|
|
||||||
document.querySelectorAll('.input-form').forEach(form => {
|
document.querySelectorAll('.input-form').forEach(form => {
|
||||||
form.addEventListener('input', () => {
|
form.addEventListener('input', () => handleUpdate(false));
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
debounceTimer = setTimeout(updateQR, 300);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Download
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener('click', resetApp);
|
||||||
|
}
|
||||||
|
|
||||||
downloadBtn.addEventListener('click', () => {
|
downloadBtn.addEventListener('click', () => {
|
||||||
const img = qrContainer.querySelector('img');
|
const format = getFormat();
|
||||||
if (img && img.src) {
|
if (format === 'png') {
|
||||||
const link = document.createElement('a');
|
const img = qrContainer.querySelector('img');
|
||||||
link.href = img.src;
|
if (img && img.src) {
|
||||||
link.download = `qrcode-${Date.now()}.png`;
|
const link = document.createElement('a');
|
||||||
document.body.appendChild(link);
|
link.href = img.src;
|
||||||
link.click();
|
link.download = `qrdamage-${Date.now()}.png`;
|
||||||
document.body.removeChild(link);
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const svgEl = qrContainer.querySelector('svg');
|
||||||
|
if (svgEl) {
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
const svgString = serializer.serializeToString(svgEl);
|
||||||
|
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `qrdamage-${Date.now()}.svg`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Init
|
// --- 7. STARTUP SEQUENCE ---
|
||||||
createQRInstance();
|
const initialMode = modeSelector.value;
|
||||||
|
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
|
||||||
|
const initialForm = document.getElementById(`form-${initialMode}`);
|
||||||
|
if (initialForm) {
|
||||||
|
initialForm.classList.add('active');
|
||||||
|
const startField = initialForm.querySelector('input, textarea');
|
||||||
|
if (startField) startField.focus();
|
||||||
|
}
|
||||||
|
handleUpdate(true);
|
||||||
});
|
});
|
||||||
|
|||||||
270
style.css
270
style.css
@@ -4,7 +4,7 @@
|
|||||||
--text-color: #000000;
|
--text-color: #000000;
|
||||||
--border-color: #000000;
|
--border-color: #000000;
|
||||||
--pane-bg: #ffffff;
|
--pane-bg: #ffffff;
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
/* Serif for the Interface/Authority */
|
/* Serif for the Interface/Authority */
|
||||||
/* --font-serif: "Georgia", "Times New Roman", Times, serif; */
|
/* --font-serif: "Georgia", "Times New Roman", Times, serif; */
|
||||||
@@ -28,13 +28,13 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LAYOUT: CSS GRID
|
/* LAYOUT: CSS GRID
|
||||||
Rows: Content (1fr) -> Footer (auto)
|
Rows: Content (1fr) -> Footer (auto)
|
||||||
*/
|
*/
|
||||||
.app-container {
|
.app-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
border-top: 5px solid var(--border-color); /* Masthead line */
|
border-top: 5px solid var(--border-color); /* Masthead line */
|
||||||
}
|
}
|
||||||
@@ -86,18 +86,28 @@ input, textarea, select {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 0; /* Crucial: No rounded corners */
|
border-radius: 0; /* Crucial: No rounded corners */
|
||||||
|
|
||||||
/* Data font */
|
/* Data font */
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
||||||
/* Reset native styles */
|
/* Reset native styles */
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Replaces the inline style for the radio container */
|
||||||
|
.format-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-color); /* Uses variable now! */
|
||||||
|
}
|
||||||
|
|
||||||
input:focus, textarea:focus, select:focus {
|
input:focus, textarea:focus, select:focus {
|
||||||
outline: 2px solid var(--text-color);
|
outline: 2px solid var(--text-color);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
@@ -127,6 +137,19 @@ details.advanced-options {
|
|||||||
background: #fdfdfd; /* Slight contrast from white */
|
background: #fdfdfd; /* Slight contrast from white */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btn-clear {
|
||||||
|
margin-top: 2rem;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-color);
|
||||||
|
/* A slightly thicker border makes it feel "bolder" as requested */
|
||||||
|
border: 2px solid var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-clear:hover:not(:disabled) {
|
||||||
|
background-color: var(--text-color);
|
||||||
|
color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -140,7 +163,7 @@ summary {
|
|||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom indicator logic if you want complete control,
|
/* Custom indicator logic if you want complete control,
|
||||||
but standard text [+]/[-] is very brutalist. */
|
but standard text [+]/[-] is very brutalist. */
|
||||||
details[open] summary {
|
details[open] summary {
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
@@ -187,20 +210,32 @@ details .control-group:last-child {
|
|||||||
width: 256px;
|
width: 256px;
|
||||||
height: 256px;
|
height: 256px;
|
||||||
margin: 0 auto 2rem auto;
|
margin: 0 auto 2rem auto;
|
||||||
|
|
||||||
/* Frame */
|
/* Frame */
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#qr-container img, #qr-container canvas {
|
/* Inside .preview-pane or relevant section */
|
||||||
|
|
||||||
|
#qr-container img,
|
||||||
|
#qr-container canvas,
|
||||||
|
#qr-container svg {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
|
||||||
image-rendering: pixelated; /* Crisp edges */
|
/* CHANGE: Force it to fill the container exactly */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* CRITICAL: This ensures upscaling looks like squares, not blur */
|
||||||
|
image-rendering: pixelated;
|
||||||
|
|
||||||
|
/* Optional: Ensure the SVG scales correctly */
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
@@ -211,7 +246,7 @@ button {
|
|||||||
color: var(--bg-color); /* White */
|
color: var(--bg-color); /* White */
|
||||||
border: 1px solid var(--text-color);
|
border: 1px solid var(--text-color);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -273,7 +308,7 @@ button:disabled {
|
|||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.app-container {
|
.app-container {
|
||||||
/* Sidebar fixed 400px, Content fluid */
|
/* Sidebar fixed 400px, Content fluid */
|
||||||
grid-template-columns: 400px 1fr;
|
grid-template-columns: 400px 1fr;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -282,20 +317,20 @@ button:disabled {
|
|||||||
.input-pane {
|
.input-pane {
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
/* Allow natural height so page scrolls to footer */
|
/* Allow natural height so page scrolls to footer */
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-pane {
|
.preview-pane {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
justify-content: center; /* Center Vertically */
|
justify-content: center; /* Center Vertically */
|
||||||
|
|
||||||
/* Engineering Paper Texture */
|
/* Engineering Paper Texture */
|
||||||
background-color: #fdfdfd;
|
background-color: #fdfdfd;
|
||||||
background-image: radial-gradient(#000 0.5px, transparent 0.5px);
|
background-image: radial-gradient(#000 0.5px, transparent 0.5px);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-wrapper {
|
.sticky-wrapper {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 2rem;
|
top: 2rem;
|
||||||
@@ -371,10 +406,10 @@ input[type="number"] {
|
|||||||
-moz-appearance: textfield; /* Remove Firefox spinner */
|
-moz-appearance: textfield; /* Remove Firefox spinner */
|
||||||
}
|
}
|
||||||
/* Remove Webkit spinners if you prefer a cleaner look */
|
/* Remove Webkit spinners if you prefer a cleaner look */
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color Group Layout */
|
/* Color Group Layout */
|
||||||
@@ -422,3 +457,198 @@ input[type="color"]::-webkit-color-swatch {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Radio Button Logic inside the bordered box */
|
||||||
|
input[type="radio"] {
|
||||||
|
/* Reset the width 100% from the general input rule */
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 50%; /* Radios can stay round, or make them 0 for squares */
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked {
|
||||||
|
background: var(--text-color); /* Fill black when checked */
|
||||||
|
box-shadow: inset 0 0 0 3px #fff; /* Create a 'donut' look */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In-Box Error Message */
|
||||||
|
.qr-error-msg {
|
||||||
|
color: red;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
/* Brutalist Box inside the Box */
|
||||||
|
border: 2px solid red;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #fff;
|
||||||
|
width: 80%; /* Don't touch the edges */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the container keeps its shape even when showing error */
|
||||||
|
#qr-container {
|
||||||
|
/* Existing styles... */
|
||||||
|
position: relative; /* Just in case */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- CAPACITY METER --- */
|
||||||
|
#capacity-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
/* Light Mode: Light Grey track */
|
||||||
|
background: #eee;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#capacity-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
/* Automatically swaps Black (Light) <-> White (Dark) */
|
||||||
|
background-color: var(--text-color);
|
||||||
|
transition: width 0.2s ease, background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific Dark Mode overrides for the track background */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#capacity-track {
|
||||||
|
/* Dark Mode: Dark Grey track so the White bar pops */
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- DARK MODE SUPPORT --- */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
/* PURE BLACK & WHITE - No "middling greys" */
|
||||||
|
--bg-color: #000000;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--border-color: #ffffff;
|
||||||
|
--pane-bg: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 1. TEXTURE: Sharper Dots */
|
||||||
|
.preview-pane {
|
||||||
|
background-color: #000000;
|
||||||
|
/* White dots on black, high contrast */
|
||||||
|
background-image: radial-gradient(#666 0.5px, transparent 0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. INPUTS: Remove grey fills, use stark borders */
|
||||||
|
input, textarea, select {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #fff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
/* Invert on focus: White Block, Black Text */
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Wrappers (Color pickers etc) */
|
||||||
|
.input-wrapper, .format-selector {
|
||||||
|
background: #000000;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. DETAILS / SUMMARY (Advanced Config) */
|
||||||
|
details.advanced-options {
|
||||||
|
background: #000000;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
details[open] summary {
|
||||||
|
/* Invert header: White Block, Black Text */
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. STICKY WRAPPER: Toned down shadow */
|
||||||
|
.sticky-wrapper {
|
||||||
|
background: #000000;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
|
||||||
|
box-shadow: 10px 10px 0px 0px #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. QR CONTAINER: Dark to match the theme */
|
||||||
|
#qr-container {
|
||||||
|
/* Now transparent/black so it doesn't look like a "flashlight" */
|
||||||
|
background-color: #000000;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the generated images (which might be white squares) are centered and clean */
|
||||||
|
#qr-container img,
|
||||||
|
#qr-container canvas,
|
||||||
|
#qr-container svg {
|
||||||
|
/* Optional: Add a thin border to the actual code so a White QR
|
||||||
|
doesn't bleed into the White Page Border if sizing matches */
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. ERROR MESSAGE: Keep it popping */
|
||||||
|
.qr-error-msg {
|
||||||
|
background-color: #000000; /* Black box */
|
||||||
|
color: #ff0000; /* Red Text */
|
||||||
|
border: 2px solid #ff0000; /* Red Border */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. ICONS & SVGS */
|
||||||
|
/* Invert Select Arrow to White */
|
||||||
|
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" fill="white"><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>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7. BUTTONS */
|
||||||
|
|
||||||
|
/* Primary Action: White Block, Black Text */
|
||||||
|
button {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
/* Hover: Invert to Black Block, White Text */
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background-color: #333;
|
||||||
|
color: #000;
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset Button: Transparent with White Border */
|
||||||
|
#btn-clear {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
#btn-clear:hover:not(:disabled) {
|
||||||
|
/* Hover: White Block, Black Text */
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user