From 79c32fd5837ebc330123e9de4784124bb2031931 Mon Sep 17 00:00:00 2001 From: pszsh Date: Mon, 2 Mar 2026 19:52:07 -0800 Subject: [PATCH] There's been a lot of changes, still in heavy early development so forgive me for not being great with the commit messages yet. I'm more apologizing in advance than for the present meaningless commit message - they'll get worse for a while. --- app.go | 136 ++++++++++++- frontend/index.html | 1 + frontend/src/main.js | 356 ++++++++++++++++++++++++++++------- frontend/src/style.css | 32 ++++ frontend/src/ui/unwrap-ui.js | 5 +- project.go | 9 + 6 files changed, 458 insertions(+), 81 deletions(-) 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