diff --git a/app.go b/app.go index d416002..a373c01 100644 --- a/app.go +++ b/app.go @@ -69,6 +69,7 @@ func (s *ImageServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { type ProjectInfoJS struct { ID string `json:"id"` Name string `json:"name"` + Type string `json:"type"` Path string `json:"path"` CreatedAt string `json:"createdAt"` HasStencil bool `json:"hasStencil"` @@ -76,12 +77,25 @@ type ProjectInfoJS struct { HasVectorWrap bool `json:"hasVectorWrap"` HasStructural bool `json:"hasStructural"` HasScanHelper bool `json:"hasScanHelper"` + HasObject bool `json:"hasObject"` BoardW float64 `json:"boardW"` BoardH float64 `json:"boardH"` ShowGrid bool `json:"showGrid"` TraditionalControls bool `json:"traditionalControls"` } +type EnclosureSetupJS struct { + GbrjobFile string `json:"gbrjobFile"` + GerberFiles []string `json:"gerberFiles"` + DrillPath string `json:"drillPath"` + NPTHPath string `json:"npthPath"` + WallThick float64 `json:"wallThick"` + WallHeight float64 `json:"wallHeight"` + Clearance float64 `json:"clearance"` + DPI float64 `json:"dpi"` + SourceDir string `json:"sourceDir"` +} + type SessionInfoJS struct { ProjectName string `json:"projectName"` BoardW float64 `json:"boardW"` @@ -184,9 +198,14 @@ func (a *App) GetRecentProjects() []ProjectInfoJS { if err != nil { continue } + pType := proj.Type + if pType == "" { + pType = "pcb" + } info := ProjectInfoJS{ ID: proj.ID, Name: proj.Name, + Type: pType, Path: e.Path, CreatedAt: e.LastOpened.Format(time.RFC3339), ShowGrid: proj.Settings.ShowGrid, @@ -209,6 +228,9 @@ func (a *App) GetRecentProjects() []ProjectInfoJS { if proj.ScanHelper != nil { info.HasScanHelper = true } + if proj.Object != nil { + info.HasObject = true + } result = append(result, info) } return result @@ -747,10 +769,13 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) { // ======== Project Lifecycle ======== -func (a *App) CreateNewProject(name string) (string, error) { +func (a *App) CreateNewProject(name, projectType string) (string, error) { if name == "" { name = "Untitled" } + if projectType == "" { + projectType = "pcb" + } safeName := sanitizeDirName(name) if safeName == "" { safeName = "untitled" @@ -767,6 +792,7 @@ func (a *App) CreateNewProject(name string) (string, error) { return "", err } proj.Name = name + proj.Type = projectType SaveProject(path, proj) a.mu.Lock() @@ -878,9 +904,14 @@ func (a *App) GetProjectInfo() *ProjectInfoJS { return nil } p := a.project + projType := p.Type + if projType == "" { + projType = "pcb" + } info := &ProjectInfoJS{ ID: p.ID, Name: p.Name, + Type: projType, Path: a.projectPath, CreatedAt: p.CreatedAt.Format(time.RFC3339), ShowGrid: p.Settings.ShowGrid, @@ -903,6 +934,9 @@ func (a *App) GetProjectInfo() *ProjectInfoJS { if p.ScanHelper != nil { info.HasScanHelper = true } + if p.Object != nil { + info.HasObject = true + } return info } @@ -1251,6 +1285,77 @@ func (a *App) GetActiveMode() string { return "" } +// ======== Enclosure Reconfigure ======== + +func (a *App) GetEnclosureSetupData() *EnclosureSetupJS { + a.mu.RLock() + defer a.mu.RUnlock() + + if a.project == nil || a.project.Enclosure == nil { + return nil + } + enc := a.project.Enclosure + var gerberFiles []string + for name := range enc.GerberFiles { + gerberFiles = append(gerberFiles, name) + } + // Reconstruct gbrjob file from gerber files map + var gbrjob string + for name := range enc.GerberFiles { + if strings.HasSuffix(strings.ToLower(name), ".gbrjob") { + gbrjob = name + break + } + } + return &EnclosureSetupJS{ + GbrjobFile: gbrjob, + GerberFiles: gerberFiles, + DrillPath: enc.DrillPath, + NPTHPath: enc.NPTHPath, + WallThick: enc.Config.WallThickness, + WallHeight: enc.Config.WallHeight, + Clearance: enc.Config.Clearance, + DPI: enc.Config.DPI, + } +} + +// ======== 3D Object Setup (Surface Wrap projects) ======== + +func (a *App) ConfigureProjectObject(modelPath string) error { + if modelPath == "" { + return fmt.Errorf("no model file selected") + } + a.mu.Lock() + if a.project == nil { + a.mu.Unlock() + return fmt.Errorf("no project open") + } + projPath := a.projectPath + a.mu.Unlock() + + baseName := filepath.Base(modelPath) + dstDir := filepath.Join(projPath, "vectorwrap") + os.MkdirAll(dstDir, 0755) + dstPath := filepath.Join(dstDir, baseName) + if err := CopyFile(modelPath, dstPath); err != nil { + return fmt.Errorf("copy model: %v", err) + } + + modelType := "stl" + if strings.HasSuffix(strings.ToLower(modelPath), ".scad") { + modelType = "scad" + } + + a.mu.Lock() + a.project.Object = &ObjectData{ + ModelFile: baseName, + ModelType: modelType, + } + a.mu.Unlock() + a.autosaveProject() + return nil +} + // ======== Vector Wrap ======== type SVGValidationResultJS struct { @@ -1623,16 +1728,27 @@ func (a *App) GetUnwrapLayout() (*UnwrapLayout, error) { return layout, nil } -func (a *App) ImportUnwrapArtwork(svgContent string) error { - a.mu.Lock() - defer a.mu.Unlock() - - if a.vectorWrapSession == nil { - a.vectorWrapSession = &VectorWrapSession{} +func (a *App) SaveUnwrapSVG() (string, error) { + svg, err := a.GetUnwrapSVG() + if err != nil { + return "", err } - // Store the artwork SVG for use with surface wrapping - a.vectorWrapSession.EnclosureSCAD = svgContent - return nil + + outDir, err := a.GetOutputDir() + if err != nil { + return "", err + } + os.MkdirAll(outDir, 0755) + + outPath := filepath.Join(outDir, "enclosure-unwrap.svg") + if err := os.WriteFile(outPath, []byte(svg), 0644); err != nil { + return "", fmt.Errorf("write SVG: %v", err) + } + return outPath, nil +} + +func (a *App) OpenFileExternal(path string) error { + return exec.Command("open", path).Start() } // ======== Structural Procedures ======== diff --git a/frontend/index.html b/frontend/index.html index 9c538ac..2e8c6c4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -48,6 +48,7 @@
+
diff --git a/frontend/src/main.js b/frontend/src/main.js index 549f668..2ecfa45 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -96,6 +96,7 @@ function navigate(page) { case 'structural': initStructural(); break; case 'scanhelper': initScanHelper(); break; case 'unwrap': initUnwrap(); break; + case 'objectsetup': initObjectSetup(); break; } } @@ -112,16 +113,19 @@ async function initLanding() {
Recent Projects
${projects.map(p => { + const typeLabel = p.type === 'surfacewrap' ? 'Surface Wrap' : 'PCB'; const modes = []; if (p.hasEnclosure) modes.push('Enclosure'); if (p.hasStencil) modes.push('Stencil'); if (p.hasVectorWrap) modes.push('Vector Wrap'); if (p.hasStructural) modes.push('Structural'); if (p.hasScanHelper) modes.push('Scan Helper'); + if (p.hasObject) modes.push('3D Object'); const modeStr = modes.length > 0 ? modes.join(', ') : 'No modes configured'; return `
${esc(p.name)} + ${typeLabel} ${esc(modeStr)}
`; }).join('')} @@ -158,30 +162,57 @@ async function initDashboard() { return; } - const modeCards = [ - { id: 'stencil', label: 'Stencil', desc: 'Solder paste stencils from gerber files', active: info.hasStencil }, - { id: 'enclosure', label: 'Enclosure', desc: 'PCB enclosures from KiCad projects', active: info.hasEnclosure }, - { id: 'vectorwrap', label: 'Vector Wrap', desc: 'Wrap 2D vector art onto 3D surfaces', active: info.hasVectorWrap }, - { id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural }, - { id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper }, - { id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded enclosure surface', active: info.hasEnclosure }, - ]; + const isSW = info.type === 'surfacewrap'; + const hasObj = info.hasObject; + + let modeCards; + if (isSW) { + modeCards = [ + { id: 'objectsetup', label: '3D Object', desc: 'Provide STL/SCAD target mesh', active: hasObj, required: !hasObj }, + { id: 'vectorwrap', label: 'Vector Wrap', desc: 'Wrap 2D vector art onto 3D surfaces', active: info.hasVectorWrap, disabled: !hasObj }, + { id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded surface', active: false, disabled: !hasObj }, + { id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural }, + { id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper }, + ]; + } else { + modeCards = [ + { id: 'stencil', label: 'Stencil', desc: 'Solder paste stencils from gerber files', active: info.hasStencil }, + { id: 'enclosure', label: 'Enclosure', desc: 'PCB enclosures from KiCad projects', active: info.hasEnclosure }, + { id: 'vectorwrap', label: 'Vector Wrap', desc: 'Wrap 2D vector art onto 3D surfaces', active: info.hasVectorWrap }, + { id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural }, + { id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper }, + { id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded enclosure surface', active: info.hasEnclosure }, + ]; + } page.innerHTML = `
- ${modeCards.map(m => ` + ${modeCards.map(m => { + if (m.disabled) { + return ` +
+

${m.label}

+

${m.desc}

+ Requires 3D Object +
`; + } + let badge = ''; + if (m.required) badge = 'Required'; + else if (m.active) badge = 'Configured'; + return `

${m.label}

${m.desc}

- ${m.active ? 'Configured' : ''} -
- `).join('')} + ${badge} +
`; + }).join('')}
`; } @@ -190,12 +221,14 @@ async function initDashboard() { async function createNewProject() { const name = await showInputDialog('New Project', 'Project name:', 'My Project'); if (name === null) return; + const projectType = await showTypeDialog(); + if (projectType === null) return; try { - const path = await wails()?.CreateNewProject(name || 'Untitled'); + const path = await wails()?.CreateNewProject(name || 'Untitled', projectType); if (!path) return; const info = await wails()?.GetProjectInfo(); if (info) { - state.project = { name: info.name, path: info.path }; + state.project = { name: info.name, path: info.path, type: info.type }; navigate('dashboard'); } } catch (e) { @@ -203,13 +236,46 @@ async function createNewProject() { } } +function showTypeDialog() { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center;'; + const box = document.createElement('div'); + box.style.cssText = 'background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;min-width:400px;text-align:center;box-shadow:0 8px 24px rgba(0,0,0,0.4);'; + box.innerHTML = ` +
Project Type
+
+
+

PCB / KiCad

+

Enclosures and stencils from gerber files

+
+
+

Surface Wrap

+

Wrap 2D art onto 3D objects

+
+
+
+ +
+ `; + overlay.appendChild(box); + document.body.appendChild(overlay); + const done = (val) => { overlay.remove(); resolve(val); }; + box.querySelectorAll('.type-option').forEach(card => { + card.onclick = () => done(card.dataset.type); + }); + box.querySelector('#type-cancel').onclick = () => done(null); + overlay.onclick = e => { if (e.target === overlay) done(null); }; + }); +} + async function openProjectDialog() { try { const path = await wails()?.OpenProjectDialog(); if (!path) return; const info = await wails()?.GetProjectInfo(); if (info) { - state.project = { name: info.name, path: info.path }; + state.project = { name: info.name, path: info.path, type: info.type }; navigate('dashboard'); } } catch (e) { @@ -367,19 +433,51 @@ function showStencilResult(files) { } // ===== Enclosure Page ===== -function initEnclosure() { +async function initEnclosure(prefill) { const page = $('#page-enclosure'); + + // If session exists and no explicit reconfigure request, go to preview + if (!prefill) { + const sessionCheck = await wails()?.GetSessionInfo(); + if (sessionCheck?.hasSession) { + navigate('preview'); + return; + } + } + state.enclosure = { gbrjobPath: '', gerberPaths: [], drillPath: '', npthPath: '', sourceDir: '' }; + // Pre-fill from existing config when reconfiguring + if (prefill) { + state.enclosure.gbrjobPath = prefill.gbrjobFile || ''; + state.enclosure.gerberPaths = prefill.gerberFiles || []; + state.enclosure.drillPath = prefill.drillPath || ''; + state.enclosure.npthPath = prefill.npthPath || ''; + state.enclosure.sourceDir = prefill.sourceDir || ''; + } + + const gbrjobLabel = state.enclosure.gbrjobPath ? state.enclosure.gbrjobPath.split('/').pop() : 'No file selected'; + const gbrjobClass = state.enclosure.gbrjobPath ? ' has-file' : ''; + const gerberLabel = state.enclosure.gerberPaths.length > 0 ? `${state.enclosure.gerberPaths.length} files` : 'No files selected'; + const gerberClass = state.enclosure.gerberPaths.length > 0 ? ' has-file' : ''; + const drillLabel = state.enclosure.drillPath ? state.enclosure.drillPath.split('/').pop() : 'No file selected'; + const drillClass = state.enclosure.drillPath ? ' has-file' : ''; + const npthLabel = state.enclosure.npthPath ? state.enclosure.npthPath.split('/').pop() : 'No file selected'; + const npthClass = state.enclosure.npthPath ? ' has-file' : ''; + const wt = prefill?.wallThick || 1.5; + const wh = prefill?.wallHeight || 10.0; + const cl = prefill?.clearance || 0.3; + const dp = prefill?.dpi || 600; + page.innerHTML = `
Gerber Job File (.gbrjob)
- No file selected + ${esc(gbrjobLabel)}
@@ -387,7 +485,7 @@ function initEnclosure() {
Gerber Files

Select a folder to auto-discover, or add files individually.

- No files selected + ${esc(gerberLabel)}
@@ -399,7 +497,7 @@ function initEnclosure() {
PTH Drill File (Optional)
- No file selected + ${esc(drillLabel)}
@@ -407,7 +505,7 @@ function initEnclosure() {
NPTH Drill File (Optional)
- No file selected + ${esc(npthLabel)}
@@ -417,19 +515,19 @@ function initEnclosure() {
Parameters
Wall Thickness (mm) - +
Wall Height (mm) - +
Clearance (mm) - +
DPI - +
@@ -577,6 +675,27 @@ async function buildEnclosure() { } } +async function reconfigureEnclosure() { + const setup = await wails()?.GetEnclosureSetupData(); + if (!setup) { + navigate('enclosure'); + return; + } + // Navigate to enclosure page with pre-fill data — bypasses the session check + $$('.page').forEach(p => hide(p)); + const el = $('#page-enclosure'); + if (el) show(el); + const modeTabs = $('#nav-mode-tabs'); + const projectName = $('#nav-project-name'); + const openOutput = $('#nav-open-output'); + if (state.project) { + if (modeTabs) modeTabs.style.display = ''; + if (projectName) { projectName.style.display = ''; projectName.textContent = state.project.name; } + if (openOutput) openOutput.style.display = ''; + } + initEnclosure(setup); +} + // ===== Enclosure Preview Page ===== async function initPreview() { const page = $('#page-preview'); @@ -593,8 +712,10 @@ async function initPreview() { page.innerHTML = `
Board Preview — ${info.boardW.toFixed(1)} × ${info.boardH.toFixed(1)} mm | ${info.sides?.length || 0} sides
@@ -780,7 +901,7 @@ async function openProject(path) { hideLoading(); const info = await wails()?.GetProjectInfo(); if (info) { - state.project = { name: info.name, path: info.path }; + state.project = { name: info.name, path: info.path, type: info.type }; navigate('dashboard'); } } catch (e) { @@ -793,6 +914,28 @@ async function openProject(path) { async function initVectorWrap() { const page = $('#page-vectorwrap'); state.vectorwrap = { svgPath: '', modelPath: '', modelType: '' }; + const isSW = state.project?.type === 'surfacewrap'; + + let modelCardHTML; + if (isSW) { + // Surface Wrap: model comes from 3D Object setup, just show its name + const info = await wails()?.GetProjectInfo(); + const objName = info?.hasObject ? 'Configured via 3D Object' : 'Not configured'; + modelCardHTML = ` +
+
3D Model
+
${esc(objName)}
+
`; + } else { + // PCB: use project enclosure directly, no external model option + modelCardHTML = ` +
+
3D Model
+
+
Checking project enclosure...
+
+
`; + } page.innerHTML = ` -
-
3D Model
- -
-

Select an STL or OpenSCAD file for the target surface.

-
- No file selected - -
-
-
+ ${modelCardHTML}
`; - try { - const vwCheck = await wails()?.GetVectorWrapInfo(); - if (vwCheck?.hasProjectEnclosure) { - const label = $('#vw-use-project-label'); - if (label) label.style.display = 'block'; + // PCB mode: auto-load project enclosure + if (!isSW) { + try { + const vwCheck = await wails()?.GetVectorWrapInfo(); + const statusEl = $('#vw-enc-status'); + if (vwCheck?.hasProjectEnclosure) { + showLoading('Loading project enclosure...'); + await wails().UseProjectEnclosureForVectorWrap(); + hideLoading(); + state.vectorwrap.modelPath = 'project-enclosure'; + state.vectorwrap.modelType = 'project-enclosure'; + if (statusEl) { + statusEl.textContent = 'Using project enclosure'; + statusEl.style.color = 'var(--success)'; + } + } else if (statusEl) { + statusEl.textContent = 'No enclosure configured — set up Enclosure first'; + statusEl.style.color = 'var(--warning)'; + } + } catch (e) { + hideLoading(); + const statusEl = $('#vw-enc-status'); + if (statusEl) { + statusEl.textContent = 'Failed to load enclosure: ' + e; + statusEl.style.color = 'var(--error)'; + } } - } catch (_) {} + } else { + // Surface Wrap: auto-set model state if object is configured + const info = await wails()?.GetProjectInfo(); + if (info?.hasObject) { + state.vectorwrap.modelPath = 'project-object'; + state.vectorwrap.modelType = 'project-object'; + } + } + updateVWOpenBtn(); } async function selectVectorWrapSVG() { @@ -995,6 +1154,58 @@ function toggleVWAssembly() { if (btn) btn.textContent = assembled ? 'Take Off Lid' : 'Put Lid On'; } +// ===== 3D Object Setup Page (Surface Wrap projects) ===== +let _objectModelPath = ''; + +function initObjectSetup() { + const page = $('#page-objectsetup'); + _objectModelPath = ''; + + page.innerHTML = ` + +
+
STL / SCAD Model File
+

Select the 3D model to use as the target surface.

+
+ No file selected + +
+
+
+
+ +
+ `; +} + +async function selectObjectModel() { + try { + const path = await wails().SelectFile('Select 3D Model', '*.stl;*.scad'); + if (!path) return; + _objectModelPath = path; + $('#obj-model-name').textContent = path.split('/').pop(); + $('#obj-model-name').classList.add('has-file'); + const btn = $('#obj-configure-btn'); + if (btn) btn.disabled = false; + } catch (e) { console.error(e); } +} + +async function configureObject() { + if (!_objectModelPath) return; + showLoading('Configuring 3D object...'); + try { + await wails().ConfigureProjectObject(_objectModelPath); + hideLoading(); + navigate('dashboard'); + } catch (e) { + hideLoading(); + alert('Failed to configure object: ' + e); + } +} + // ===== Unwrap Page ===== let _unwrapSVG = ''; @@ -1018,15 +1229,20 @@ async function initUnwrap() { `; } +let _unwrapSavedPath = ''; + async function generateUnwrap() { try { showLoading('Generating unwrap template...'); - const svg = await wails()?.GetUnwrapSVG(); + const path = await wails()?.SaveUnwrapSVG(); hideLoading(); - if (!svg) { + if (!path) { alert('No enclosure session active. Set up an enclosure first.'); return; } + _unwrapSavedPath = path; + + const svg = await wails()?.GetUnwrapSVG(); _unwrapSVG = svg; const preview = $('#unwrap-preview'); if (preview) { @@ -1038,35 +1254,36 @@ async function generateUnwrap() { svgEl.style.maxHeight = '100%'; } } - const exportBtn = $('#unwrap-export-btn'); - if (exportBtn) exportBtn.disabled = false; + + const status = $('#unwrap-status'); + if (status) { + status.style.display = 'block'; + status.textContent = 'Saved: ' + path.split('/').pop(); + } + const openBtn = $('#unwrap-open-btn'); + if (openBtn) openBtn.disabled = false; + const folderBtn = $('#unwrap-folder-btn'); + if (folderBtn) folderBtn.disabled = false; } catch (e) { hideLoading(); alert('Failed to generate unwrap: ' + e); } } -function exportUnwrapSVG() { - if (!_unwrapSVG) return; - const blob = new Blob([_unwrapSVG], { type: 'image/svg+xml' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'enclosure-unwrap.svg'; - a.click(); - URL.revokeObjectURL(url); +async function openUnwrapExternal() { + if (!_unwrapSavedPath) return; + try { + await wails()?.OpenFileExternal(_unwrapSavedPath); + } catch (e) { + alert('Failed to open file: ' + e); + } } -async function importUnwrapArtwork() { +async function openUnwrapFolder() { try { - const path = await wails()?.SelectFile('Select Artwork SVG', '*.svg'); - if (!path) return; - showLoading('Importing artwork...'); - await wails()?.ImportUnwrapArtwork(path); - hideLoading(); + await wails()?.OpenOutputFolder(); } catch (e) { - hideLoading(); - alert('Import failed: ' + e); + alert('Failed to open folder: ' + e); } } @@ -2773,9 +2990,10 @@ Object.assign(window, { setVWOverlayOpacity, setVWOverlayZ, applyVWTransform, applyVWGridRes, toggleVWGrid, resetVWGrid, selectStructuralSVG, generateStructural, updateStructuralLive, renderStructuralPreview, applyScanPagePreset, generateScanSheet, showScanResult, - generateUnwrap, exportUnwrapSVG, importUnwrapArtwork, + generateUnwrap, openUnwrapExternal, openUnwrapFolder, toggleUnwrapFolds, toggleUnwrapLabels, toggleUnwrapCutouts, toggleVWCameraLock, + selectObjectModel, configureObject, reconfigureEnclosure, }); // ===== Init ===== diff --git a/frontend/src/style.css b/frontend/src/style.css index 72b7516..6b9b6ce 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -266,6 +266,38 @@ body { letter-spacing: 0.5px; font-weight: 600; } +.badge-warn { + color: var(--warning, #f9e2af); + border-color: var(--warning, #f9e2af); +} +.badge-muted { + color: var(--text-subtle); + border-color: var(--border); + opacity: 0.7; +} + +.card-disabled { + pointer-events: none; + opacity: 0.35; +} + +.project-type-badge { + font-size: 9px; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.3px; + font-weight: 600; + vertical-align: middle; +} +.badge-pcb { + background: color-mix(in srgb, var(--accent) 15%, transparent); + color: var(--accent); +} +.badge-sw { + background: color-mix(in srgb, var(--success, #a6e3a1) 15%, transparent); + color: var(--success, #a6e3a1); +} /* Dashboard grid */ .dashboard-grid { diff --git a/frontend/src/ui/unwrap-ui.js b/frontend/src/ui/unwrap-ui.js index 1a814cc..caaad51 100644 --- a/frontend/src/ui/unwrap-ui.js +++ b/frontend/src/ui/unwrap-ui.js @@ -3,8 +3,9 @@ export function buildUnwrapSidebar() {
Unwrap Template
- - + + +
Display