Skip to content

Commit 53d9961

Browse files
committed
feat: Phase 9 - 2nd information column with vertical separator
1 parent aa47edb commit 53d9961

4 files changed

Lines changed: 188 additions & 10 deletions

File tree

frontend/src/i18n/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@
394394
"dsQrCustomUrl": "Eigene URL",
395395
"dsInfo": "Informationen",
396396
"dsInfoSize": "Textgröße (mm)",
397+
"dsInfo2Show": "2. Informationsspalte",
398+
"dsInfo2Vsep": "Vertikaler Trenner",
397399
"dsLeft": "Links",
398400
"dsRight": "Rechts",
399401
"dsTop": "Oben",

frontend/src/i18n/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@
392392
"dsQrCustomUrl": "Custom URL",
393393
"dsInfo": "Information",
394394
"dsInfoSize": "Text size (mm)",
395+
"dsInfo2Show": "2nd Information Column",
396+
"dsInfo2Vsep": "Vertical separator",
395397
"dsLeft": "Left",
396398
"dsRight": "Right",
397399
"dsTop": "Top",

frontend/src/pages/spools/[id]/print.astro

Lines changed: 183 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,58 @@ const { id } = Astro.params
316316
<textarea id="ds-info-tpl" class="fm-input ds-tpl-input" rows="7" placeholder="{filament.material}&#10;{filament.color}&#10;Ext: {filament.extruder_temp}°C&#10;Bed: {filament.bed_temp}°C"></textarea>
317317
<div class="ds-token-hints" id="ds-info-tokens"></div>
318318
</div>
319+
320+
<!-- Second info column toggle -->
321+
<div class="ds-info2-toggle">
322+
<label class="fm-checkbox-group">
323+
<input type="checkbox" id="ds-info2-show">
324+
<span id="dsl-info2-show">2nd Information Column</span>
325+
</label>
326+
</div>
327+
328+
<!-- Second info column controls (collapsed by default) -->
329+
<div id="ds-info2-block" class="ds-info2-block" style="display:none">
330+
<div class="ds-info2-divider"></div>
331+
<label class="fm-checkbox-group">
332+
<input type="checkbox" id="ds-info2-vsep">
333+
<span id="dsl-info2-vsep">Vertical separator</span>
334+
</label>
335+
<div class="ds-size-row">
336+
<label class="fm-label ds-size-label" id="dsl-info2-size">Text size (mm)</label>
337+
<input type="range" id="ds-info2-size-range" min="1" max="10" step="0.5" value="2.5" class="ds-slider">
338+
<input type="number" id="ds-info2-size" value="2.5" step="0.5" min="1" max="10" class="fm-input ds-size-num">
339+
</div>
340+
<div>
341+
<label class="fm-label" id="dsl-info2-halign">Horizontal align</label>
342+
<div class="ds-align-group" id="ds-info2-align-group" data-value="left">
343+
<button class="ds-align-btn active" data-align="left" title="Left">
344+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="18" y2="18"/></svg>
345+
</button>
346+
<button class="ds-align-btn" data-align="center" title="Center">
347+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>
348+
</button>
349+
<button class="ds-align-btn" data-align="right" title="Right">
350+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="6" y1="18" x2="21" y2="18"/></svg>
351+
</button>
352+
</div>
353+
</div>
354+
<div>
355+
<label class="fm-label" id="dsl-info2-valign">Vertical align</label>
356+
<div class="ds-pill-group" id="ds-info2-valign-group" data-value="bottom">
357+
<button class="ds-pill-btn" data-val="top" id="dsip2-top">Top</button>
358+
<button class="ds-pill-btn" data-val="center" id="dsip2-mid">Mid</button>
359+
<button class="ds-pill-btn active" data-val="bottom" id="dsip2-bot">Bot</button>
360+
</div>
361+
</div>
362+
<div>
363+
<div class="ds-tpl-label-row">
364+
<label class="fm-label" id="dsl-info2-tpl">Template</label>
365+
<button class="ds-syntax-btn" id="ds-info2-syntax-btn" type="button" title="Template syntax help">?</button>
366+
</div>
367+
<textarea id="ds-info2-tpl" class="fm-input ds-tpl-input" rows="5" placeholder="{filament.weight}g&#10;{filament.color_hex}"></textarea>
368+
<div class="ds-token-hints" id="ds-info2-tokens"></div>
369+
</div>
370+
</div>
319371
</div>
320372
</details>
321373

@@ -976,6 +1028,24 @@ const { id } = Astro.params
9761028
gap: 12px 20px;
9771029
}
9781030

1031+
/* Info second-column toggle + sub-block */
1032+
.ds-info2-toggle {
1033+
border-top: 1px solid var(--border);
1034+
padding-top: 8px;
1035+
margin-top: 4px;
1036+
}
1037+
1038+
.ds-info2-block {
1039+
display: flex;
1040+
flex-direction: column;
1041+
gap: 12px;
1042+
}
1043+
1044+
.ds-info2-divider {
1045+
border-top: 1px dashed var(--border);
1046+
margin: 2px 0;
1047+
}
1048+
9791049
/* Show checkbox embedded in section header */
9801050
.ds-header-show {
9811051
display: flex;
@@ -1359,9 +1429,8 @@ const { id } = Astro.params
13591429
btn.addEventListener('click', () => activateTab(btn.dataset.tab as 'print' | 'designer'))
13601430
})
13611431

1362-
// Restore last active tab
1432+
// Restore last active tab — deferred until after designer setup completes
13631433
const savedTab = localStorage.getItem(ACTIVE_TAB_KEY)
1364-
if (savedTab === 'designer') activateTab('designer')
13651434

13661435
// ─── Label Designer ────────────────────────────────────────────────────────
13671436

@@ -1373,6 +1442,7 @@ const { id } = Astro.params
13731442
title: { show: boolean; sizeMm: number; fitToWidth: boolean; align: 'left'|'center'|'right'; template: string; dividerAbove: boolean; dividerBelow: boolean }
13741443
qr: { mode: 'none'|'simple'|'icon'; sizeMm: number; position: 'left'|'right'; vAlign: 'top'|'center'|'bottom'; linkMode: 'spool'|'url'; urlTemplate: string }
13751444
info: { show: boolean; sizeMm: number; hAlign: 'left'|'center'|'right'; vAlign: 'top'|'center'|'bottom'; template: string }
1445+
info2: { show: boolean; vsep: boolean; sizeMm: number; hAlign: 'left'|'center'|'right'; vAlign: 'top'|'center'|'bottom'; template: string }
13761446
}
13771447

13781448
const DESIGNER_DEFAULTS: LabelDesignerSettings = {
@@ -1382,6 +1452,7 @@ const { id } = Astro.params
13821452
qr: { mode: 'simple', sizeMm: 18, position: 'right', vAlign: 'bottom', linkMode: 'spool', urlTemplate: '' },
13831453
info: { show: true, sizeMm: 2.5, hAlign: 'left', vAlign: 'bottom',
13841454
template: '{filament.material}\n{filament.color}\nExt: {filament.extruder_temp}°C\nBed: {filament.bed_temp}°C' },
1455+
info2: { show: false, vsep: false, sizeMm: 2.5, hAlign: 'left', vAlign: 'bottom', template: '' },
13851456
}
13861457

13871458
// All available tokens for the token-chip hints
@@ -1427,6 +1498,14 @@ const { id } = Astro.params
14271498
vAlign: (document.getElementById('ds-info-valign-group') as HTMLElement).dataset.value as 'top'|'center'|'bottom',
14281499
template: (document.getElementById('ds-info-tpl') as HTMLTextAreaElement).value,
14291500
},
1501+
info2: {
1502+
show: (document.getElementById('ds-info2-show') as HTMLInputElement).checked,
1503+
vsep: (document.getElementById('ds-info2-vsep') as HTMLInputElement).checked,
1504+
sizeMm: Number((document.getElementById('ds-info2-size') as HTMLInputElement).value),
1505+
hAlign: (document.getElementById('ds-info2-align-group') as HTMLElement).dataset.value as 'left'|'center'|'right',
1506+
vAlign: (document.getElementById('ds-info2-valign-group') as HTMLElement).dataset.value as 'top'|'center'|'bottom',
1507+
template: (document.getElementById('ds-info2-tpl') as HTMLTextAreaElement).value,
1508+
},
14301509
}
14311510
localStorage.setItem(DESIGNER_KEY, JSON.stringify(s))
14321511
}
@@ -1445,6 +1524,7 @@ const { id } = Astro.params
14451524
title: { ...DESIGNER_DEFAULTS.title, ...parsed.title },
14461525
qr: { ...DESIGNER_DEFAULTS.qr, ...parsed.qr },
14471526
info: { ...DESIGNER_DEFAULTS.info, ...parsed.info },
1527+
info2: { ...DESIGNER_DEFAULTS.info2, ...parsed.info2 },
14481528
}
14491529
}
14501530
} catch { /* ignore */ }
@@ -1482,6 +1562,16 @@ const { id } = Astro.params
14821562
setAlignGroup('ds-info-align-group', s.info.hAlign)
14831563
setPillGroup('ds-info-valign-group', s.info.vAlign)
14841564
;(document.getElementById('ds-info-tpl') as HTMLTextAreaElement).value = s.info.template
1565+
1566+
const info2 = s.info2 ?? DESIGNER_DEFAULTS.info2
1567+
;(document.getElementById('ds-info2-show') as HTMLInputElement).checked = info2.show
1568+
;(document.getElementById('ds-info2-block') as HTMLElement).style.display = info2.show ? '' : 'none'
1569+
;(document.getElementById('ds-info2-vsep') as HTMLInputElement).checked = info2.vsep ?? false
1570+
;(document.getElementById('ds-info2-size') as HTMLInputElement).value = String(info2.sizeMm)
1571+
;(document.getElementById('ds-info2-size-range') as HTMLInputElement).value = String(info2.sizeMm)
1572+
setAlignGroup('ds-info2-align-group', info2.hAlign)
1573+
setPillGroup('ds-info2-valign-group', info2.vAlign)
1574+
;(document.getElementById('ds-info2-tpl') as HTMLTextAreaElement).value = info2.template
14851575
}
14861576

14871577
// Helpers for button groups
@@ -1550,19 +1640,26 @@ const { id } = Astro.params
15501640
wireAlignGroup('ds-logo-align-group')
15511641
wireAlignGroup('ds-title-align-group')
15521642
wireAlignGroup('ds-info-align-group')
1643+
wireAlignGroup('ds-info2-align-group')
15531644
wirePillGroup('ds-qr-pos-group')
15541645
wirePillGroup('ds-qr-valign-group')
15551646
wirePillGroup('ds-info-valign-group')
1647+
wirePillGroup('ds-info2-valign-group')
15561648
wirePillGroup('ds-qr-link-group', (val) => {
15571649
;(document.getElementById('ds-qr-url-wrap') as HTMLElement).style.display = val === 'url' ? '' : 'none'
15581650
})
15591651

1560-
const dsInputIds = ['ds-width','ds-height','ds-logo-size','ds-title-size','ds-qr-size','ds-info-size','ds-title-tpl','ds-qr-url-tpl','ds-info-tpl']
1561-
const dsCheckIds = ['ds-border','ds-logo-show','ds-title-show','ds-title-fit','ds-divider-above','ds-divider-below','ds-info-show']
1652+
const dsInputIds = ['ds-width','ds-height','ds-logo-size','ds-title-size','ds-qr-size','ds-info-size','ds-info2-size','ds-title-tpl','ds-qr-url-tpl','ds-info-tpl','ds-info2-tpl']
1653+
const dsCheckIds = ['ds-border','ds-logo-show','ds-title-show','ds-title-fit','ds-divider-above','ds-divider-below','ds-info-show','ds-info2-show','ds-info2-vsep']
15621654
const dsSelectIds = ['ds-qr-mode']
15631655
;[...dsInputIds, ...dsCheckIds, ...dsSelectIds].forEach(id => {
15641656
document.getElementById(id)?.addEventListener('change', () => { saveDesignerSettings(); updateDesignerPreview() })
15651657
})
1658+
// Toggle info2 block visibility when its show-checkbox changes
1659+
document.getElementById('ds-info2-show')?.addEventListener('change', (e) => {
1660+
const show = (e.target as HTMLInputElement).checked
1661+
;(document.getElementById('ds-info2-block') as HTMLElement).style.display = show ? '' : 'none'
1662+
})
15661663
dsInputIds.forEach(id => {
15671664
const el = document.getElementById(id)
15681665
if (el && (el as HTMLInputElement).type !== 'checkbox') {
@@ -1576,6 +1673,7 @@ const { id } = Astro.params
15761673
['ds-title-size-range', 'ds-title-size'],
15771674
['ds-qr-size-range', 'ds-qr-size'],
15781675
['ds-info-size-range', 'ds-info-size'],
1676+
['ds-info2-size-range', 'ds-info2-size'],
15791677
]
15801678
for (const [rangId, numId] of sliderPairs) {
15811679
const rang = document.getElementById(rangId) as HTMLInputElement
@@ -1650,10 +1748,12 @@ const { id } = Astro.params
16501748
}
16511749
document.getElementById('ds-title-syntax-btn')?.addEventListener('click', (e) => { e.stopPropagation(); openSyntaxPopover(e.currentTarget as HTMLElement) })
16521750
document.getElementById('ds-info-syntax-btn')?.addEventListener('click', (e) => { e.stopPropagation(); openSyntaxPopover(e.currentTarget as HTMLElement) })
1751+
document.getElementById('ds-info2-syntax-btn')?.addEventListener('click', (e) => { e.stopPropagation(); openSyntaxPopover(e.currentTarget as HTMLElement) })
16531752

16541753
// Token chips
16551754
buildTokenChips('ds-title-tokens', 'ds-title-tpl', DESIGNER_TOKENS)
16561755
buildTokenChips('ds-info-tokens', 'ds-info-tpl', DESIGNER_TOKENS)
1756+
buildTokenChips('ds-info2-tokens', 'ds-info2-tpl', DESIGNER_TOKENS)
16571757
buildTokenChips('ds-qr-tokens', 'ds-qr-url-tpl', ['{id}', '{filament.name}'])
16581758

16591759
// Load persisted designer settings
@@ -1826,6 +1926,7 @@ const { id } = Astro.params
18261926
title: { ...DESIGNER_DEFAULTS.title, ...preset.settings.title },
18271927
qr: { ...DESIGNER_DEFAULTS.qr, ...preset.settings.qr },
18281928
info: { ...DESIGNER_DEFAULTS.info, ...preset.settings.info },
1929+
info2: { ...DESIGNER_DEFAULTS.info2, ...(preset.settings.info2 ?? {}) },
18291930
}
18301931
// Apply to form
18311932
;(document.getElementById('ds-logo-show') as HTMLInputElement).checked = s.logo.show
@@ -1857,6 +1958,15 @@ const { id } = Astro.params
18571958
setAlignGroup('ds-info-align-group', s.info.hAlign)
18581959
setPillGroup('ds-info-valign-group', s.info.vAlign)
18591960
;(document.getElementById('ds-info-tpl') as HTMLTextAreaElement).value = s.info.template
1961+
const _pi2 = s.info2 ?? DESIGNER_DEFAULTS.info2
1962+
;(document.getElementById('ds-info2-show') as HTMLInputElement).checked = _pi2.show
1963+
;(document.getElementById('ds-info2-block') as HTMLElement).style.display = _pi2.show ? '' : 'none'
1964+
;(document.getElementById('ds-info2-vsep') as HTMLInputElement).checked = _pi2.vsep ?? false
1965+
;(document.getElementById('ds-info2-size') as HTMLInputElement).value = String(_pi2.sizeMm)
1966+
;(document.getElementById('ds-info2-size-range') as HTMLInputElement).value = String(_pi2.sizeMm)
1967+
setAlignGroup('ds-info2-align-group', _pi2.hAlign)
1968+
setPillGroup('ds-info2-valign-group', _pi2.vAlign)
1969+
;(document.getElementById('ds-info2-tpl') as HTMLTextAreaElement).value = _pi2.template
18601970
saveDesignerSettings()
18611971
_activePresetName = name
18621972
presetNameInput.value = name
@@ -1996,6 +2106,15 @@ const { id } = Astro.params
19962106
setAlignGroup('ds-info-align-group', d.hAlign)
19972107
setPillGroup('ds-info-valign-group', d.vAlign)
19982108
;(document.getElementById('ds-info-tpl') as HTMLTextAreaElement).value = d.template
2109+
const d2 = DESIGNER_DEFAULTS.info2
2110+
;(document.getElementById('ds-info2-show') as HTMLInputElement).checked = d2.show
2111+
;(document.getElementById('ds-info2-block') as HTMLElement).style.display = d2.show ? '' : 'none'
2112+
;(document.getElementById('ds-info2-vsep') as HTMLInputElement).checked = d2.vsep
2113+
;(document.getElementById('ds-info2-size') as HTMLInputElement).value = String(d2.sizeMm)
2114+
;(document.getElementById('ds-info2-size-range') as HTMLInputElement).value = String(d2.sizeMm)
2115+
setAlignGroup('ds-info2-align-group', d2.hAlign)
2116+
setPillGroup('ds-info2-valign-group', d2.vAlign)
2117+
;(document.getElementById('ds-info2-tpl') as HTMLTextAreaElement).value = d2.template
19992118
})
20002119

20012120
// ─── End Label Designer ────────────────────────────────────────────────────
@@ -2161,6 +2280,15 @@ const { id } = Astro.params
21612280
document.getElementById('dsip-mid').textContent = t('spools.dsMid', 'Mid')
21622281
document.getElementById('dsip-bot').textContent = t('spools.dsBot', 'Bot')
21632282
document.getElementById('dsl-info-tpl').textContent = t('spools.dsTemplate', 'Template')
2283+
document.getElementById('dsl-info2-show').textContent = t('spools.dsInfo2Show', '2nd Information Column')
2284+
document.getElementById('dsl-info2-vsep').textContent = t('spools.dsInfo2Vsep', 'Vertical separator')
2285+
document.getElementById('dsl-info2-size').textContent = t('spools.dsInfoSize', 'Text size (mm)')
2286+
document.getElementById('dsl-info2-halign').textContent = t('spools.dsHAlign', 'Horizontal align')
2287+
document.getElementById('dsl-info2-valign').textContent = t('spools.dsVAlign', 'Vertical align')
2288+
document.getElementById('dsip2-top').textContent = t('spools.dsTop', 'Top')
2289+
document.getElementById('dsip2-mid').textContent = t('spools.dsMid', 'Mid')
2290+
document.getElementById('dsip2-bot').textContent = t('spools.dsBot', 'Bot')
2291+
document.getElementById('dsl-info2-tpl').textContent = t('spools.dsTemplate', 'Template')
21642292
document.getElementById('label-width-text').textContent = (window as any).__t('spools.labelWidth')
21652293
document.getElementById('label-height-text').textContent = (window as any).__t('spools.labelHeight')
21662294
document.getElementById('label-font-size-text').textContent = (window as any).__t('spools.fontSize')
@@ -2422,7 +2550,19 @@ const { id } = Astro.params
24222550
const s = (() => {
24232551
try {
24242552
const raw = localStorage.getItem(DESIGNER_KEY)
2425-
if (raw) return { ...DESIGNER_DEFAULTS, ...JSON.parse(raw) }
2553+
if (raw) {
2554+
const parsed = JSON.parse(raw)
2555+
return {
2556+
...DESIGNER_DEFAULTS,
2557+
...parsed,
2558+
logo: { ...DESIGNER_DEFAULTS.logo, ...parsed.logo },
2559+
label: { ...DESIGNER_DEFAULTS.label, ...parsed.label },
2560+
title: { ...DESIGNER_DEFAULTS.title, ...parsed.title },
2561+
qr: { ...DESIGNER_DEFAULTS.qr, ...parsed.qr },
2562+
info: { ...DESIGNER_DEFAULTS.info, ...parsed.info },
2563+
info2: { ...DESIGNER_DEFAULTS.info2, ...parsed.info2 },
2564+
}
2565+
}
24262566
} catch { /* ignore */ }
24272567
return DESIGNER_DEFAULTS
24282568
})()
@@ -2525,12 +2665,16 @@ const { id } = Astro.params
25252665
mainRow.style.padding = '1mm'
25262666
mainRow.style.gap = '1.5mm'
25272667
mainRow.style.flexDirection = s.qr.position === 'left' ? 'row-reverse' : 'row'
2528-
mainRow.style.alignItems = (s.info.show && s.info.template.trim())
2529-
? (s.info.vAlign === 'top' ? 'flex-start' : s.info.vAlign === 'center' ? 'center' : 'flex-end')
2530-
: 'stretch'
2668+
mainRow.style.alignItems = (() => {
2669+
const v = (s.info.show && s.info.template.trim()) ? s.info.vAlign
2670+
: (s.info2?.show && s.info2.template.trim()) ? s.info2.vAlign
2671+
: null
2672+
if (!v) return 'stretch'
2673+
return v === 'top' ? 'flex-start' : v === 'center' ? 'center' : 'flex-end'
2674+
})()
25312675
labelPreview.appendChild(mainRow)
25322676

2533-
// Information block
2677+
// Information block (primary)
25342678
if (s.info.show && s.info.template.trim()) {
25352679
const infoEl = document.createElement('div')
25362680
infoEl.style.flex = '1'
@@ -2540,12 +2684,38 @@ const { id } = Astro.params
25402684
infoEl.style.color = 'black'
25412685
infoEl.style.overflow = 'hidden'
25422686
infoEl.style.lineHeight = '1.4'
2543-
25442687
const frag = parseTemplate(s.info.template, data)
25452688
infoEl.appendChild(frag)
25462689
mainRow.appendChild(infoEl)
25472690
}
25482691

2692+
// Information block 2 (second column)
2693+
const hasInfo2 = s.info2?.show && s.info2.template.trim()
2694+
if (hasInfo2) {
2695+
// Vertical separator between info1 and info2
2696+
if (s.info2.vsep) {
2697+
const sep = document.createElement('div')
2698+
sep.style.width = '1px'
2699+
sep.style.alignSelf = 'stretch'
2700+
sep.style.minHeight = '100%'
2701+
sep.style.background = 'black'
2702+
sep.style.flexShrink = '0'
2703+
mainRow.appendChild(sep)
2704+
}
2705+
const info2El = document.createElement('div')
2706+
info2El.style.flex = '1'
2707+
info2El.style.minWidth = '0'
2708+
info2El.style.fontSize = (s.info2.sizeMm * 3.78) + 'px'
2709+
info2El.style.textAlign = s.info2.hAlign
2710+
info2El.style.color = 'black'
2711+
info2El.style.overflow = 'hidden'
2712+
info2El.style.lineHeight = '1.4'
2713+
info2El.style.alignSelf = s.info2.vAlign === 'top' ? 'flex-start' : s.info2.vAlign === 'center' ? 'center' : 'flex-end'
2714+
const frag2 = parseTemplate(s.info2.template, data)
2715+
info2El.appendChild(frag2)
2716+
mainRow.appendChild(info2El)
2717+
}
2718+
25492719
// QR Code block
25502720
if (s.qr.mode !== 'none') {
25512721
const qrWrap = document.createElement('div')
@@ -2647,6 +2817,9 @@ const { id } = Astro.params
26472817
updatePreview()
26482818
})
26492819

2820+
// Restore active tab now that all designer state is fully initialized
2821+
if (savedTab === 'designer') activateTab('designer')
2822+
26502823
// Initialize: load logo then render whichever tab is active
26512824
loadLabelLogo().then(() => {
26522825
const activeTab = localStorage.getItem(ACTIVE_TAB_KEY)

version.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.2.9

0 commit comments

Comments
 (0)