feat: expand layout editor — full-screen, rulers, bottom controls
- Modal enlarged to min(1400px, 96vw) × 92vh - Canvas fills full width; field controls moved to a strip below - Inch rulers on top and left: major ticks at every inch (labeled), half-inch, quarter-inch, and eighth-inch subdivisions - Layout editor button hidden on mobile/portrait screens (canvas requires landscape space to be usable)
This commit is contained in:
+17
-4
@@ -88,10 +88,18 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-wide { width: min(720px, 96vw); }
|
.modal-wide { width: min(720px, 96vw); }
|
||||||
.modal-layout-editor { width: min(980px, 96vw); }
|
.modal-layout-editor { width: min(1400px, 96vw); height: 92vh; }
|
||||||
.layout-editor-body { flex-direction: row !important; padding: 0 !important; gap: 0; overflow: hidden; min-height: 340px; }
|
.layout-editor-body { flex: 1; min-height: 0; flex-direction: column !important; padding: 0 !important; gap: 0; overflow: hidden; }
|
||||||
#layout-canvas-container { flex: 1; min-width: 0; padding: 12px; overflow: hidden; background: var(--bg); }
|
.layout-canvas-area { flex: 1; min-height: 0; display: grid; grid-template-columns: 24px 1fr; grid-template-rows: 24px 1fr; }
|
||||||
#layout-sidebar { width: 200px; flex-shrink: 0; padding: 12px; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; overflow-y: auto; background: var(--surface); }
|
.layout-ruler-corner { background: var(--surface); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||||||
|
#layout-ruler-top { background: var(--surface); border-bottom: 1px solid var(--border); overflow: hidden; }
|
||||||
|
#layout-ruler-left { background: var(--surface); border-right: 1px solid var(--border); overflow: hidden; }
|
||||||
|
#layout-canvas-container { overflow: hidden; background: #d4d4d4; }
|
||||||
|
.layout-controls { flex-shrink: 0; display: flex; align-items: center; gap: 14px; padding: 8px 16px; border-top: 1px solid var(--border); background: var(--surface); flex-wrap: wrap; }
|
||||||
|
.layout-coord { display: flex; align-items: center; gap: 4px; }
|
||||||
|
.layout-coord label { font-size: 11px; font-weight: 600; text-transform: uppercase; color: var(--text-muted); white-space: nowrap; margin: 0; }
|
||||||
|
.layout-coord input { width: 68px; }
|
||||||
|
.layout-coord .frac { font-size: 11px; color: var(--text-muted); min-width: 28px; }
|
||||||
|
|
||||||
.qbo-tabs {
|
.qbo-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -816,3 +824,8 @@ input[type="file"] {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.account-checkbox-label:hover { border-color: var(--primary); }
|
.account-checkbox-label:hover { border-color: var(--primary); }
|
||||||
|
|
||||||
|
/* Hide layout editor button on portrait/mobile — canvas needs landscape space */
|
||||||
|
@media (max-width: 768px), (orientation: portrait) {
|
||||||
|
#btn-layout-editor { display: none !important; }
|
||||||
|
}
|
||||||
|
|||||||
+41
-43
@@ -752,62 +752,60 @@
|
|||||||
<button id="btn-close-layout-editor" class="btn-icon" title="Close">×</button>
|
<button id="btn-close-layout-editor" class="btn-icon" title="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body layout-editor-body">
|
<div class="modal-body layout-editor-body">
|
||||||
<div id="layout-canvas-container"><!-- SVG rendered by JS --></div>
|
<!-- Canvas with rulers -->
|
||||||
<div id="layout-sidebar">
|
<div class="layout-canvas-area">
|
||||||
<div class="form-group">
|
<div class="layout-ruler-corner"></div>
|
||||||
<label for="layout-field-select">Field</label>
|
<div id="layout-ruler-top"></div>
|
||||||
<select id="layout-field-select" style="font-size:12px"></select>
|
<div id="layout-ruler-left"></div>
|
||||||
|
<div id="layout-canvas-container"><!-- SVG rendered by JS --></div>
|
||||||
|
</div>
|
||||||
|
<!-- Controls strip -->
|
||||||
|
<div class="layout-controls">
|
||||||
|
<div class="layout-coord" style="gap:6px">
|
||||||
|
<label for="layout-field-select" style="font-size:11px;font-weight:600;text-transform:uppercase;color:var(--text-muted)">Field</label>
|
||||||
|
<select id="layout-field-select" style="font-size:12px;max-width:200px"></select>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer">
|
<label style="display:flex;align-items:center;gap:5px;font-size:12px;cursor:pointer;white-space:nowrap">
|
||||||
<input type="checkbox" id="layout-field-visible"> Visible
|
<input type="checkbox" id="layout-field-visible"> Visible
|
||||||
</label>
|
</label>
|
||||||
<div class="form-group">
|
<div style="width:1px;height:24px;background:var(--border)"></div>
|
||||||
<label for="layout-field-x">X Position</label>
|
<div class="layout-coord">
|
||||||
<div style="display:flex;align-items:center;gap:6px">
|
<label for="layout-field-x">X</label>
|
||||||
<input type="number" id="layout-field-x" step="0.0625" min="0" max="8.5" style="width:72px">
|
<input type="number" id="layout-field-x" step="0.0625" min="0" max="8.5">
|
||||||
<span id="layout-field-x-frac" style="font-size:11px;color:var(--text-muted)"></span>
|
<span class="frac" id="layout-field-x-frac"></span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="layout-coord">
|
||||||
<label for="layout-field-y">Y Position</label>
|
<label for="layout-field-y">Y</label>
|
||||||
<div style="display:flex;align-items:center;gap:6px">
|
<input type="number" id="layout-field-y" step="0.0625" min="0" max="3.5">
|
||||||
<input type="number" id="layout-field-y" step="0.0625" min="0" max="3.5" style="width:72px">
|
<span class="frac" id="layout-field-y-frac"></span>
|
||||||
<span id="layout-field-y-frac" style="font-size:11px;color:var(--text-muted)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="layout-end-pos-group" hidden>
|
<div id="layout-end-pos-group" style="display:contents" hidden>
|
||||||
<div class="form-group">
|
<div class="layout-coord">
|
||||||
<label for="layout-field-x2">End X</label>
|
<label for="layout-field-x2">End X</label>
|
||||||
<div style="display:flex;align-items:center;gap:6px">
|
<input type="number" id="layout-field-x2" step="0.0625" min="0" max="8.5">
|
||||||
<input type="number" id="layout-field-x2" step="0.0625" min="0" max="8.5" style="width:72px">
|
<span class="frac" id="layout-field-x2-frac"></span>
|
||||||
<span id="layout-field-x2-frac" style="font-size:11px;color:var(--text-muted)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="layout-coord">
|
||||||
<label for="layout-field-y2">End Y</label>
|
<label for="layout-field-y2">End Y</label>
|
||||||
<div style="display:flex;align-items:center;gap:6px">
|
<input type="number" id="layout-field-y2" step="0.0625" min="0" max="3.5">
|
||||||
<input type="number" id="layout-field-y2" step="0.0625" min="0" max="3.5" style="width:72px">
|
<span class="frac" id="layout-field-y2-frac"></span>
|
||||||
<span id="layout-field-y2-frac" style="font-size:11px;color:var(--text-muted)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style="width:1px;height:24px;background:var(--border)"></div>
|
||||||
<div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px">Nudge ¹⁄₁₆"</div>
|
<!-- Nudge cross -->
|
||||||
<div style="display:grid;grid-template-columns:repeat(3,28px);grid-template-rows:repeat(3,28px);gap:3px;justify-content:center">
|
<div style="display:flex;flex-direction:column;align-items:center;gap:2px">
|
||||||
<span></span>
|
<div style="font-size:10px;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em">¹⁄₁₆"</div>
|
||||||
<button id="nudge-up" class="btn-sm btn-secondary" style="padding:0;text-align:center">↑</button>
|
<div style="display:grid;grid-template-columns:repeat(3,22px);grid-template-rows:repeat(3,22px);gap:2px">
|
||||||
<span></span>
|
<span></span><button id="nudge-up" class="btn-sm btn-secondary" style="padding:0;display:flex;align-items:center;justify-content:center">↑</button><span></span>
|
||||||
<button id="nudge-left" class="btn-sm btn-secondary" style="padding:0;text-align:center">←</button>
|
<button id="nudge-left" class="btn-sm btn-secondary" style="padding:0;display:flex;align-items:center;justify-content:center">←</button>
|
||||||
<span></span>
|
|
||||||
<button id="nudge-right" class="btn-sm btn-secondary" style="padding:0;text-align:center">→</button>
|
|
||||||
<span></span>
|
|
||||||
<button id="nudge-down" class="btn-sm btn-secondary" style="padding:0;text-align:center">↓</button>
|
|
||||||
<span></span>
|
<span></span>
|
||||||
|
<button id="nudge-right" class="btn-sm btn-secondary" style="padding:0;display:flex;align-items:center;justify-content:center">→</button>
|
||||||
|
<span></span><button id="nudge-down" class="btn-sm btn-secondary" style="padding:0;display:flex;align-items:center;justify-content:center">↓</button><span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="layout-save-status" style="font-size:11px;color:var(--text-muted);text-align:center;min-height:14px"></div>
|
<div id="layout-save-status" style="font-size:11px;color:var(--text-muted);min-width:56px"></div>
|
||||||
<div style="margin-top:auto;padding-top:8px;border-top:1px solid var(--border)">
|
<div style="margin-left:auto">
|
||||||
<button id="btn-layout-reset" class="btn-secondary btn-sm" style="width:100%;font-size:11px" data-admin-only>↺ Reset to Default</button>
|
<button id="btn-layout-reset" class="btn-secondary btn-sm" data-admin-only>↺ Reset to Default</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+54
-4
@@ -1938,7 +1938,7 @@ function svgEl(tag, attrs, text) {
|
|||||||
|
|
||||||
function renderLayoutCanvas() {
|
function renderLayoutCanvas() {
|
||||||
const container = document.getElementById('layout-canvas-container');
|
const container = document.getElementById('layout-canvas-container');
|
||||||
const W = container.offsetWidth - 24;
|
const W = container.offsetWidth;
|
||||||
if (W <= 0) return;
|
if (W <= 0) return;
|
||||||
const SCALE = W / 8.5;
|
const SCALE = W / 8.5;
|
||||||
layoutState.scale = SCALE;
|
layoutState.scale = SCALE;
|
||||||
@@ -1947,13 +1947,13 @@ function renderLayoutCanvas() {
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
const svg = svgEl('svg', { width: W, height: H, style: 'display:block;user-select:none' });
|
const svg = svgEl('svg', { width: W, height: H, style: 'display:block;user-select:none' });
|
||||||
|
|
||||||
// Check boundary and background
|
// White check background
|
||||||
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#ccc', 'stroke-width':1 }));
|
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
||||||
|
|
||||||
// MICR reference line
|
// MICR reference line
|
||||||
const micrY = (3.5 - 0.267) * SCALE;
|
const micrY = (3.5 - 0.267) * SCALE;
|
||||||
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
||||||
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':8, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
||||||
|
|
||||||
for (const f of layoutState.fields) {
|
for (const f of layoutState.fields) {
|
||||||
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
||||||
@@ -1962,6 +1962,56 @@ function renderLayoutCanvas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(svg);
|
container.appendChild(svg);
|
||||||
|
renderRulers(W, H, SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRulers(W, H, scale) {
|
||||||
|
const RULER = 24;
|
||||||
|
const topEl = document.getElementById('layout-ruler-top');
|
||||||
|
const leftEl = document.getElementById('layout-ruler-left');
|
||||||
|
if (!topEl || !leftEl) return;
|
||||||
|
|
||||||
|
// ── Horizontal ruler (top) ──────────────────────────────────────
|
||||||
|
const topSvg = svgEl('svg', { width: W, height: RULER, style: 'display:block' });
|
||||||
|
topSvg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:RULER, fill:'var(--surface)' }));
|
||||||
|
|
||||||
|
for (let n8 = 0; n8 <= Math.ceil(8.5 * 8); n8++) {
|
||||||
|
const inches = n8 / 8;
|
||||||
|
if (inches > 8.5) break;
|
||||||
|
const x = inches * scale;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isHalf = n8 % 4 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const tickH = isInch ? RULER - 2 : isHalf ? 14 : isQtr ? 9 : 5;
|
||||||
|
topSvg.appendChild(svgEl('line', { x1:x, y1:RULER, x2:x, y2:RULER - tickH, stroke:'#999', 'stroke-width': isInch ? 1 : 0.5 }));
|
||||||
|
if (isInch && inches > 0) {
|
||||||
|
topSvg.appendChild(svgEl('text', { x:x + 2, y:RULER - tickH - 1, 'font-size':8, fill:'#666', 'font-family':'sans-serif' }, inches + '"'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
topEl.innerHTML = '';
|
||||||
|
topEl.appendChild(topSvg);
|
||||||
|
|
||||||
|
// ── Vertical ruler (left) ───────────────────────────────────────
|
||||||
|
const leftSvg = svgEl('svg', { width: RULER, height: H, style: 'display:block' });
|
||||||
|
leftSvg.appendChild(svgEl('rect', { x:0, y:0, width:RULER, height:H, fill:'var(--surface)' }));
|
||||||
|
|
||||||
|
for (let n8 = 0; n8 <= Math.ceil(3.5 * 8); n8++) {
|
||||||
|
const inches = n8 / 8;
|
||||||
|
if (inches > 3.5) break;
|
||||||
|
const y = inches * scale;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isHalf = n8 % 4 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const tickW = isInch ? RULER - 2 : isHalf ? 14 : isQtr ? 9 : 5;
|
||||||
|
leftSvg.appendChild(svgEl('line', { x1:RULER, y1:y, x2:RULER - tickW, y2:y, stroke:'#999', 'stroke-width': isInch ? 1 : 0.5 }));
|
||||||
|
if (isInch && inches > 0) {
|
||||||
|
const t = svgEl('text', { 'font-size':8, fill:'#666', 'font-family':'sans-serif',
|
||||||
|
'text-anchor':'end', x: RULER - tickW - 2, y: y + 3 }, inches + '"');
|
||||||
|
leftSvg.appendChild(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leftEl.innerHTML = '';
|
||||||
|
leftEl.appendChild(leftSvg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFieldSvgElement(f, scale, selected) {
|
function createFieldSvgElement(f, scale, selected) {
|
||||||
|
|||||||
Reference in New Issue
Block a user