diff --git a/InteractiveHtmlBom/web/ibom.css b/InteractiveHtmlBom/web/ibom.css index a601c3f..7092dac 100644 --- a/InteractiveHtmlBom/web/ibom.css +++ b/InteractiveHtmlBom/web/ibom.css @@ -19,6 +19,15 @@ --track-color-highlight: #D04040; --zone-color: #def5f1; --zone-color-highlight: #d0404080; + + --toast-bg: rgba(215,215,215,0.2); + --toast-border: #ffb629; + --toast-text: #222; + --toast-muted: #666; + --toast-header-border: rgba(0,0,0,0.12); + + --toast-primary: #42c642; + --toast-primary-hover: #30a130; } html, @@ -35,6 +44,11 @@ body { --pin1-outline-color-highlight: #ccff00; --track-color: #42524f; --zone-color: #42524f; + --toast-bg: rgba(215,215,215,0.20); + --toast-border: #ffb629; + --toast-text: #f3f4f6; + --toast-muted: #ffb629; + --toast-header-border: rgba(255,255,255,0.17); background-color: #252c30; color: #eee; } @@ -885,4 +899,180 @@ a { ::-moz-focus-inner { padding: 0; +} + +.copy-link-button { + font-size: 1em; + border: none; + height: 1.5em; + background: transparent; + background-color: transparent !important; + cursor: pointer; + vertical-align: middle; +} + +.copy-link-button:hover { + opacity: 0.3; +} + +.copy-status { + display: inline-block; + margin-left: 5px; + padding: 2px 6px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + + opacity: 0; + visibility: hidden; + transition: opacity 0.4s ease; +} + +.copy-status.visible { + opacity: 1; + visibility: visible; +} + +.reference-warning-toast { + position: fixed; + top: 16px; + right: 16px; + + width: 420px; + max-width: calc(100vw - 32px); + + border: 2px solid var(--toast-border); + border-left: 4px var(--toast-border); + + border-radius: 12px; + + box-shadow: + 0 10px 25px rgba(0, 0, 0, 0.12), + 0 2px 8px rgba(0, 0, 0, 0.06); + + z-index: 10000; + + overflow: hidden; + + animation: toast-slide-in 0.25s ease-out; + + backdrop-filter: blur(10px); + background: var(--toast-bg); +} + +.toast-header { + display: flex; + align-items: center; + gap: 10px; + + padding: 14px 16px; + + border-bottom: 2px solid var(--toast-header-border); +} + +.toast-icon { + font-size: 18px; +} + +.toast-title { + flex: 1; + font-weight: 600; + font-size: 15px; + color: var(--toast-border); +} + +.toast-close { + border: none; + background: transparent; + cursor: pointer; + + color: #f3f4f6; + font-size: 22px; + line-height: 1; + + padding: 0; +} + +.toast-close:hover { + color: #ffb629; +} + +.toast-body { + padding: 16px; + color: var(--toast-text); + line-height: 1.5; +} + +.toast-body p { + margin: 0; +} + +.toast-body p + p { + margin-top: 8px; +} + +.toast-note { + color: var(--toast-muted); + font-size: 14px; +} + +.toast-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + + padding: 0 16px 16px; +} + +.btn { + border: none; + border-radius: 8px; + + padding: 10px 16px; + + font-size: 14px; + font-weight: 500; + + cursor: pointer; + + width: auto; + height: auto; + + transition: + background-color 0.15s, + transform 0.1s; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + background: #42c642; + color: white; +} + +.btn-primary:hover { + background: #30A130; +} + +.btn-secondary { + background: #f3f4f6; + color: #111827; +} + +.btn-secondary:hover { + background: #e5e7eb; +} + +@keyframes toast-slide-in { + from { + opacity: 0; + transform: translateX(24px); + } + + to { + opacity: 1; + transform: translateX(0); + } } \ No newline at end of file diff --git a/InteractiveHtmlBom/web/ibom.js b/InteractiveHtmlBom/web/ibom.js index d628d98..79dc1f4 100644 --- a/InteractiveHtmlBom/web/ibom.js +++ b/InteractiveHtmlBom/web/ibom.js @@ -671,6 +671,13 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { netname = bomentry; td = document.createElement("TD"); td.innerHTML = highlightFilter(netname ? netname : "<no net>"); + + // Add copy button for net names in netlist mode + if (settings.bommode === "netlist") { + var copyButton = createCopyButton("net", netname); + td.appendChild(copyButton); + } + tr.appendChild(td); var color = settings.netColors[netname] || defaultNetColor; td = document.createElement("TD"); @@ -721,7 +728,16 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { } } else if (column === "References") { td = document.createElement("TD"); - td.innerHTML = highlightFilter(references.map(r => r[0]).join(", ")); + var refHtml = highlightFilter(references.map(r => r[0]).join(", ")); + td.innerHTML = refHtml; + + // Add copy button for component references in ungrouped mode + if (settings.bommode === "ungrouped") { + var copyButton = createCopyButton("ref", references[0][0]); + td.appendChild(copyButton); + } + + tr.appendChild(td); } else if (column === "Quantity" && settings.bommode == "grouped") { // Quantity @@ -783,6 +799,229 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { }); } +function createCopyButton(type, value) { + var copyButton = document.createElement("button"); + copyButton.className = "copy-link-button"; + copyButton.title = "Copy deep-link URL"; + copyButton.innerHTML = "🔗"; + + // status box for visual feedback + var statusBox = document.createElement("span"); + statusBox.className = "copy-status"; + statusBox.textContent = "Copied!"; + + // Cache footprint references for faster lookups (only build if needed) + var footprintRefCache = null; + + function buildUrl() { + var url = new URL(window.location.href); + + if (type === "net") { + url.searchParams.set("net", value); + } else if (type === "ref") { + // For reference links, also include the ID to make it more robust + url.searchParams.set("ref", value); + + // Try to find the corresponding ID for this reference + var id = null; + if (pcbdata && pcbdata.footprints) { + // Build cache if needed + if (!footprintRefCache) { + footprintRefCache = {}; + for (var i = 0; i < pcbdata.footprints.length; i++) { + footprintRefCache[pcbdata.footprints[i].ref] = i; + } + } + + // Use the cache to get ID quickly + if (footprintRefCache[value] !== undefined) { + id = footprintRefCache[value]; + } + } + if (id !== null) { + url.searchParams.set("id", id); + } + } + + return url.toString(); + } + + // Left-click -> copy + copyButton.addEventListener("click", function (e) { + e.stopPropagation(); + + var url = buildUrl(); + copyToClipboard(url); + + statusBox.classList.add("visible"); + + clearTimeout(statusBox.hideTimer); + + statusBox.hideTimer = setTimeout(function () { + statusBox.classList.remove("visible"); + }, 2000); + }); + + // Right-click -> open in new window + copyButton.addEventListener("mousedown", function (e) { + if (e.button === 1) { + e.preventDefault(); + e.stopPropagation(); + + window.open(buildUrl(), "_blank"); + } + }); + + // return with a wrapper + var wrapper = document.createElement("span"); + wrapper.appendChild(copyButton); + wrapper.appendChild(statusBox); + + return wrapper; +} + +function validateReferenceForId(ref, id) { + // Validate that the reference corresponds to the given ID in pcbdata + if (!pcbdata || !pcbdata.footprints || id >= pcbdata.footprints.length) { + return false; + } + + // Check if the footprint at the specified ID has the correct reference + return pcbdata.footprints[id].ref === ref; +} + +function showReferenceMismatchWarning(ref, id) { + // Ha már van ilyen figyelmeztetés, töröljük + document + .querySelectorAll(".reference-warning-toast") + .forEach(el => el.remove()); + + const toast = document.createElement("div"); + toast.className = "reference-warning-toast"; + + toast.innerHTML = ` +
+ Reference ${escapeHtml(ref)} does not match the + expected reference for ID ${id}. +
+ ++ This link may be stale. Choose which value to use. +
+