{"id":32361,"date":"2026-04-07T08:54:31","date_gmt":"2026-04-07T08:54:31","guid":{"rendered":"https:\/\/meber2026.graphoservice.it\/spare-parts-view\/"},"modified":"2026-05-13T07:22:37","modified_gmt":"2026-05-13T07:22:37","slug":"spare-parts-view","status":"publish","type":"page","link":"https:\/\/www.meber.it\/en\/spare-parts-view\/","title":{"rendered":"Spare Parts View"},"content":{"rendered":"<div class=\"et_pb_section_0 et_pb_section et_section_regular et_flex_section\">\n<div class=\"et_pb_row_0 et_pb_row et_flex_row\">\n<div class=\"et_pb_column_0 et_pb_column et-last-child et_flex_column et_pb_css_mix_blend_mode_passthrough et_flex_column_24_24 et_flex_column_12_24_tablet et_flex_column_24_24_phone\">\n<div class=\"et_pb_group_0 et_pb_group et_pb_module et_flex_group et_pb_css_mix_blend_mode_passthrough\">\n<div class=\"et_pb_icon_0 et_pb_icon et_animated et_pb_module et_flex_module\"><span class=\"et_pb_icon_wrap\"><span class=\"et-pb-icon\">\uf0ad<\/span><\/span><\/div>\n\n<div class=\"et_pb_text_0 et_pb_text et_pb_bg_layout_dark et_clickable et_pb_module et_flex_module\"><div class=\"et_pb_text_inner\"><p>Product List<\/p>\n<\/div><\/div>\n\n<div class=\"et_pb_text_1 et_pb_text et_pb_bg_layout_dark et_animated et_pb_module et_block_module\"><div class=\"et_pb_text_inner\"><h1 id=\"spb-product-title\"><\/h1>\n<\/div><\/div>\n\n<div class=\"et_pb_text_2 et_pb_text et_pb_bg_layout_dark et_animated et_pb_module et_block_module\"><div class=\"et_pb_text_inner\"><section class=\"spb-hero-head\">\n<h1 id=\"spb-hero-product-title\">Product name<\/h1>\n<p id=\"spb-hero-product-subtitle\">Spare Parts Procurement Page<\/p>\n<\/section>\n<\/div><\/div>\n<\/div>\n\n<div class=\"et_pb_group_1 et_pb_group et_pb_module et_flex_group et_pb_css_mix_blend_mode_passthrough\">\n<div class=\"et_pb_text_3 et_pb_text et_pb_bg_layout_light et_pb_module et_flex_module\"><div class=\"et_pb_text_inner\"><p><img decoding=\"async\" id=\"spb-hero-product-image\" src=\"\" alt=\"\" style=\"max-width:15vw;\"><\/p>\n<\/div><\/div>\n<\/div>\n\n<div class=\"et_pb_image_0 et_pb_image et_pb_module et_flex_module\"><span class=\"et_pb_image_wrap\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.meber.it\/wp-content\/uploads\/2026\/02\/baffo_bianco.svg\" title=\"baffo_bianco\" width=\"1600\" height=\"200\" srcset=\"https:\/\/www.meber.it\/wp-content\/uploads\/2026\/02\/baffo_bianco.svg 1600w\" sizes=\"(min-width: 0px) and (max-width: 480px) 480px, (min-width: 481px) and (max-width: 980px) 980px, (min-width: 981px) and (max-width: 1280px) 1280px, (min-width: 1281px) 1600px, 100vw\" class=\"wp-image-127\" \/><\/span><\/div>\n<\/div>\n<\/div>\n<\/div>\n\n<div class=\"et_pb_section_1 et_pb_section et_section_regular et_flex_section\">\n<div class=\"et_pb_row_1 et_pb_row et_flex_row\">\n<div class=\"et_pb_column_1 et_pb_column et-last-child et_flex_column et_pb_css_mix_blend_mode_passthrough et_flex_column_24_24 et_flex_column_24_24_tablet et_flex_column_24_24_phone\">\n<div class=\"et_pb_code_0 et_pb_code et_pb_module\"><div class=\"et_pb_code_inner\"><header class=\"topbar\">\n  <div class=\"brand-wrap\">\n    <img decoding=\"async\" src=\"https:\/\/www.meber.it\/website\/images\/logo-MeBer.svg\" width=\"140\" alt=\"Me.Ber Logo\">\n    <span class=\"brand-subtitle\">Spare Parts Procurement System<\/span>\n  <\/div>\n\n  <div class=\"search-cart\">\n    <div class=\"searchbox\">\n      <input id=\"q\" type=\"search\" placeholder=\"Search by code, description or position...\" autocomplete=\"off\" aria-label=\"Cerca\">\n      <div id=\"suggestions\" class=\"suggestions\"><\/div>\n    <\/div>\n\n    <button id=\"cart-toggle\" class=\"cart-button\" aria-expanded=\"false\" aria-controls=\"cart-panel\">\n      <span class=\"cart-icon\" aria-hidden=\"true\">\ud83d\uded2<\/span>\n<span class=\"cart-label\">Cart<\/span>\n<span id=\"cart-count\" class=\"cart-count\">0<\/span>\n    <\/button>\n  <\/div>\n<\/header>\n\n<section id=\"cart-panel\" class=\"cart-panel\" hidden=\"\">\n  <div id=\"cart\"><\/div>\n  <div class=\"cart-actions\">\n    <button id=\"checkout\" class=\"btn-primary\">Export order<\/button>\n  <\/div>\n<\/section>\n\n<section class=\"spb-hero-head\">\n  <div class=\"spb-breadcrumb\" id=\"spb-breadcrumb\" style=\"display:none;\"><\/div>\n  <div class=\"spb-active-group\" id=\"spb-active-group\" style=\"display:none;\"><\/div>\n  <p id=\"spb-hero-product-description\" style=\"display:none;\"><\/p>\n  <img decoding=\"async\" id=\"spb-hero-product-image\" src=\"\" alt=\"\" style=\"display:none;\">\n<\/section>\n\n<main class=\"layout spb-layout\">\n  <aside class=\"panel tree\">\n    <h2>Browse<\/h2>\n    <div id=\"tree\"><\/div>\n  <\/aside>\n\n  <section class=\"panel drawing\">\n    <div class=\"panel-head\">\n      <h2>View<\/h2>\n    <\/div>\n    <div id=\"drawing\"><\/div>\n  <\/section>\n\n  <section class=\"panel list\">\n    <h2>Components<\/h2>\n    <div id=\"list\"><\/div>\n  <\/section>\n<\/main>\n\n<div id=\"lightbox\" class=\"lightbox\" hidden=\"\">\n  <div class=\"lightbox-backdrop\"><\/div>\n  <div class=\"lightbox-body\">\n    <button class=\"lb-close\" aria-label=\"Chiudi\">\u00d7<\/button>\n    <img id=\"lb-img\" alt=\"Component preview\">\n    <div id=\"lb-caption\" class=\"muted\"><\/div>\n  <\/div>\n<\/div><\/div><\/div>\n\n<div class=\"et_pb_code_1 et_pb_code et_pb_module\"><div class=\"et_pb_code_inner\"><style>:root{\n  --gap: clamp(10px,2vw,18px);\n  --radius: 14px;\n  --bg: #f6f7f9;\n  --muted: #6b7280;\n  --card: #fff;\n  --accent: rgb(255, 175, 0);\n  --dark: #111;\n  --tree-accent: #004F8A;\n}\n\n*{box-sizing:border-box}\n\nbody{\n  margin:0;\n  font-family:\"Inter\",system-ui,-apple-system,\"Segoe UI\",Roboto,Arial,sans-serif;\n  background:var(--bg);\n  color:var(--dark);\n}\n\nh1,h2,h3{\n  font-weight:800;\n  letter-spacing:-0.01em;\n}\n\na{\n  text-decoration:none;\n  color:inherit;\n}\n\n.muted{\n  opacity:.55;\n}\n\n\/* TOPBAR *\/\n.topbar{\n  display:grid;\n  grid-template-columns:1fr auto;\n  gap:12px;\n  align-items:center;\n  padding:10px 14px;\n  background:#fff;\n  border-bottom:1px solid #eaeaea;\n  box-shadow:0 2px 12px rgba(0,0,0,.04);\n  position:sticky;\n  top:0;\n  z-index:30;\n}\n\n.brand-wrap{\n  display:flex;\n  align-items:center;\n  gap:12px;\n}\n\n.brand-subtitle{\n  font-size:14px;\n  font-weight:500;\n  color:#555;\n  letter-spacing:.03em;\n  border-left:1px solid #ddd;\n  padding-left:10px;\n  line-height:1.2;\n}\n\n.search-cart{\n  display:flex;\n  align-items:center;\n  gap:12px;\n}\n\n.searchbox{\n  position:relative;\n  width:min(560px,60vw);\n}\n\n.searchbox input[type=\"search\"]{\n  width:100%;\n  padding:10px 12px;\n  border:1px solid #ddd;\n  border-radius:10px;\n}\n\n.suggestions{\n  position:absolute;\n  left:0;\n  right:0;\n  top:100%;\n  background:#fff;\n  border:1px solid #e5e7eb;\n  border-top:none;\n  display:none;\n  max-height:50vh;\n  overflow:auto;\n  border-radius:0 0 12px 12px;\n  padding:8px;\n  z-index:25;\n  box-shadow:0 12px 24px rgba(0,0,0,.08);\n}\n\n.suggestions .sug{\n  padding:10px 12px;\n  border-radius:8px;\n  cursor:pointer;\n}\n\n.suggestions .sug + .sug{\n  margin-top:6px;\n}\n\n.suggestions .sug:hover,\n.suggestions .sug.active{\n  background:#f6f7f9;\n}\n\n.cart-button{\n  display:inline-flex;\n  align-items:center;\n  gap:8px;\n  border:1px solid #e5e7eb;\n  background:#fff;\n  padding:8px 12px;\n  border-radius:999px;\n  cursor:pointer;\n}\n\n.cart-count{\n  min-width:22px;\n  height:22px;\n  line-height:22px;\n  padding:0 6px;\n  border-radius:999px;\n  background:var(--accent);\n  color:#fff;\n  font-weight:700;\n  font-size:12px;\n  text-align:center;\n}\n\n.cart-panel{\n  position:absolute;\n  right:16px;\n  top:calc(64px + 8px);\n  width:min(520px,92vw);\n  background:#fff;\n  border:1px solid #e5e7eb;\n  border-radius:12px;\n  box-shadow:0 16px 40px rgba(0,0,0,.12);\n  z-index:40;\n  padding:12px;\n}\n\n.cart-panel[hidden]{\n  display:none;\n}\n\n.cart-actions{\n  margin-top:10px;\n  display:flex;\n  justify-content:flex-end;\n}\n\n.btn-primary{\n  background:var(--accent);\n  color:#fff;\n  border:none;\n  padding:10px 14px;\n  border-radius:10px;\n  cursor:pointer;\n}\n\n.cart-line{\n  display:grid;\n  grid-template-columns:1fr auto auto;\n  gap:10px;\n  align-items:center;\n  padding:8px;\n  border-bottom:1px solid #eee;\n}\n\n.total{\n  display:flex;\n  justify-content:space-between;\n  margin-top:10px;\n  font-weight:700;\n}\n\n\/* HERO *\/\n.spb-hero-head{\n  margin:18px 20px 12px;\n}\n\n.spb-breadcrumb{\n  display:flex;\n  flex-wrap:wrap;\n  gap:8px;\n  align-items:center;\n  margin:0 0 10px;\n  font-size:13px;\n  color:#667085;\n}\n\n.spb-breadcrumb__item{\n  color:#667085;\n}\n\n.spb-breadcrumb__sep{\n  color:#b3b9c4;\n}\n\n.spb-active-group{\n  display:block;\n  margin:0 0 8px;\n  font-size:15px;\n  font-weight:700;\n  color:var(--tree-accent);\n  text-transform:uppercase;\n  letter-spacing:.04em;\n}\n\n#spb-hero-product-title{\n  margin:0;\n  font-size:clamp(34px,4vw,72px);\n  line-height:.95;\n  font-weight:300;\n  letter-spacing:-.02em;\n  text-transform:uppercase;\n}\n\n#spb-hero-product-subtitle{\n  margin:10px 0 0;\n  font-size:18px;\n}\n\n#spb-hero-product-description{\n  margin:12px 0 0;\n  font-size:16px;\n  line-height:1.45;\n  max-width:720px;\n}\n\n#spb-hero-product-image{\n  display:block;\n  max-width:min(100%,560px);\n  width:100%;\n  height:auto;\n}\n\n\/* LAYOUT *\/\n.spb-layout{\n  display:grid;\n  grid-template-columns:260px minmax(0,1fr) minmax(340px,420px);\n  gap:24px;\n  padding:var(--gap) 20px 24px;\n  align-items:start;\n}\n\n@media (max-width:1700px){\n  .spb-layout{\n    grid-template-columns:240px minmax(0,1fr) minmax(320px,390px);\n  }\n}\n\n@media (max-width:1350px){\n  .spb-layout{\n    grid-template-columns:220px minmax(0,1fr) minmax(300px,360px);\n  }\n}\n\n@media (max-width:1200px){\n  .spb-layout{\n    grid-template-columns:1fr;\n  }\n}\n\n\/* PANELS *\/\n.panel{\n  background:var(--card);\n  border-radius:16px;\n  padding:14px;\n  box-shadow:0 4px 16px rgba(0,0,0,.05);\n  min-height:0;\n}\n\n.panel h2{\n  margin:0 0 10px;\n  font-size:14px;\n  color:var(--muted);\n  font-weight:600;\n  letter-spacing:.02em;\n  text-transform:uppercase;\n}\n\n.panel-head{\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  gap:10px;\n  margin-bottom:8px;\n}\n\n.panel-head h2{\n  margin:0;\n}\n\n.panel.tree h2,\n.panel.list h2,\n.panel.drawing .panel-head{\n  position:sticky;\n  top:0;\n  background:#fff;\n  z-index:3;\n  padding-bottom:8px;\n}\n\n\/* TREE VIEW PUBLIC CLEAN *\/\n.panel.tree{\n  max-height:calc(100vh - 280px);\n  overflow:auto;\n  padding:12px;\n  background:#fff;\n}\n\n#tree{\n  font-size:13px;\n  line-height:1.35;\n}\n\n.tree-root,\n.tree-item__children{\n  list-style:none;\n  margin:0;\n  padding:0;\n}\n\n.tree-item{\n  margin:0;\n}\n\n.tree-item__children{\n  display:none;\n  margin-left:18px;\n  padding-left:10px;\n}\n\n.tree-item.is-open > .tree-item__children{\n  display:block;\n}\n\n.tree-row{\n  display:grid;\n  grid-template-columns:16px 1fr;\n  align-items:start;\n  gap:8px;\n  padding:7px 10px;\n  border-radius:8px;\n  transition:background .15s ease;\n}\n\n.tree-row:hover{\n  background:#f7f8fa;\n}\n\n.tree-item.is-active > .tree-row{\n  background:#eef3f8;\n}\n\n.tree-toggle{\n  width:16px;\n  height:16px;\n  border:none;\n  background:transparent;\n  padding:0;\n  margin:0;\n  cursor:pointer;\n  color:#6f7a86;\n  font-size:12px;\n  line-height:1;\n  display:grid;\n  place-items:center;\n  margin-top:1px;\n}\n\n.tree-toggle.is-placeholder{\n  visibility:hidden;\n  pointer-events:none;\n}\n\n.tree-title{\n  background:none;\n  border:none;\n  padding:0;\n  margin:0;\n  text-align:left;\n  cursor:pointer;\n  font:inherit;\n  color:#2b2f36;\n  line-height:1.45;\n}\n\n.tree-item.is-active > .tree-row .tree-title{\n  font-weight:700;\n  color:#111;\n}\n\n.tree-leaf-icon{\n  width:16px;\n  height:16px;\n  display:grid;\n  place-items:center;\n  font-size:12px;\n  color:#8a94a3;\n  margin-top:1px;\n}\n\n.panel.tree::-webkit-scrollbar{\n  width:8px;\n}\n.panel.tree::-webkit-scrollbar-thumb{\n  background:#d6dbe1;\n  border-radius:999px;\n}\n.panel.tree::-webkit-scrollbar-track{\n  background:transparent;\n}\n\n\/* DRAWING PANEL PUBLIC *\/\n.panel.drawing{\n  overflow:hidden;\n  background:#fff;\n}\n\n.dwg-toolbar{\n  display:flex;\n  gap:8px;\n  align-items:center;\n  justify-content:flex-end;\n  margin-bottom:10px;\n}\n\n.dwg-toolbar button{\n  border:1px solid #ddd;\n  background:#fff;\n  border-radius:8px;\n  padding:6px 10px;\n  cursor:pointer;\n}\n\n.dwg-toolbar button:hover{\n  background:#f7f7f7;\n}\n\n.dwg-wrap{\n  width:100%;\n  background:#fff;\n  border:1px solid #e5e7eb;\n  border-radius:16px;\n  padding:14px;\n  overflow:hidden;\n}\n\n.dwg-stage{\n  position:relative;\n  width:100%;\n  aspect-ratio:16 \/ 9;\n  min-height:0;\n  height:min(62vh,740px);\n  max-height:740px;\n  background:#fff;\n  border-radius:12px;\n  overflow:hidden;\n}\n\n@media (max-width:1500px){\n  .dwg-stage{\n    height:min(56vh,680px);\n    max-height:680px;\n  }\n}\n\n@media (max-width:1200px){\n  .dwg-stage{\n    height:auto;\n    min-height:520px;\n    max-height:none;\n  }\n}\n\n.dwg-scene{\n  width:100%;\n  height:100%;\n  display:block;\n}\n\n.dwg-scene-image{\n  pointer-events:none;\n}\n\n.dwg-connector-line{\n  stroke:#8f96a3;\n  stroke-width:1.5;\n  fill:none;\n  vector-effect:non-scaling-stroke;\n}\n\n.dwg-hotspot-circle{\n  fill:#fff;\n  stroke:#000;\n  stroke-width:2;\n  vector-effect:non-scaling-stroke;\n}\n\n.dwg-hotspot-text{\n  fill:#000;\n  font-weight:700;\n  text-anchor:middle;\n  dominant-baseline:middle;\n  pointer-events:none;\n  user-select:none;\n}\n\n.dwg-hotspot.is-active .dwg-hotspot-circle,\n.dwg-hotspot.is-hovered .dwg-hotspot-circle{\n  fill:#ffaf00;\n}\n\n.dwg-hotspot.is-active .dwg-hotspot-text,\n.dwg-hotspot.is-hovered .dwg-hotspot-text{\n  fill:#fff;\n}\n\n.dwg-hotspot-hit{\n  fill:transparent;\n  cursor:pointer;\n}\n\n\/* LIST *\/\n.panel.list{\n  max-height:calc(100vh - 280px);\n  overflow:auto;\n}\n\n.list-item{\n  display:grid;\n  grid-template-columns:68px 1fr 96px;\n  gap:12px;\n  align-items:start;\n  padding:14px;\n  border-radius:12px;\n  background:#fff;\n  margin-bottom:10px;\n  box-shadow:0 1px 0 rgba(0,0,0,.05);\n  border:1px solid #ececec;\n}\n\n.list-item.is-active,\n.list-item.is-hovered{\n  outline:2px solid rgba(255,175,0,.8);\n  background:#fffdf7;\n}\n\n.pos{\n  text-align:center;\n  padding-top:2px;\n}\n\n.pos-label{\n  font-size:10px;\n  opacity:.5;\n}\n\n.pos-num{\n  font-size:18px;\n  font-weight:700;\n  color:#18b63c;\n  line-height:1.05;\n  word-break:break-word;\n}\n\n.meta{\n  min-width:0;\n}\n\n.code-badge{\n  display:inline-block;\n  padding:6px 10px;\n  background:#f2f3f5;\n  border-radius:8px;\n  font-family:ui-monospace, Menlo, Consolas, monospace;\n  font-size:14px;\n  letter-spacing:.02em;\n  color:#111;\n}\n\n.meta .title{\n  margin-top:10px;\n  display:flex;\n  align-items:flex-start;\n  gap:8px;\n}\n\n.meta .title strong{\n  font-size:14px;\n  font-weight:800;\n  color:#111;\n  line-height:1.25;\n}\n\n.group{\n  margin-top:4px;\n  color:#8a9099;\n  text-transform:uppercase;\n  letter-spacing:.03em;\n  font-weight:300;\n  font-size:11px;\n}\n\n.qty{\n  display:flex;\n  flex-direction:column;\n  align-items:stretch;\n  gap:8px;\n  min-width:0;\n}\n\n.qty input{\n  width:100%;\n  padding:6px 8px;\n  border:1px solid #d9dce1;\n  border-radius:10px;\n}\n\n.btn-ghost{\n  width:100%;\n  padding:8px 10px;\n  border:1px solid #d9dce1;\n  border-radius:10px;\n  background:var(--accent);\n  color:#fff;\n  cursor:pointer;\n  text-align:center;\n}\n\n.btn-ghost:hover{\n  filter:brightness(.96);\n}\n\n.img-btn{\n  border:none;\n  background:none;\n  cursor:pointer;\n  font-size:16px;\n  line-height:1;\n}\n\n@media (max-width:1200px){\n  .panel.tree,\n  .panel.list{\n    max-height:none;\n    overflow:visible;\n  }\n}\n\n@media (max-width:820px){\n  .list-item{\n    grid-template-columns:78px 1fr;\n    align-items:flex-start;\n  }\n\n  .list-item .qty{\n    grid-column:2 \/ 3;\n    margin-top:8px;\n    max-width:180px;\n  }\n}\n\n\/* LIGHTBOX *\/\n.lightbox{\n  position:fixed;\n  inset:0;\n  display:grid;\n  place-items:center;\n  z-index:50;\n}\n\n.lightbox[hidden]{\n  display:none;\n}\n\n.lightbox-backdrop{\n  position:absolute;\n  inset:0;\n  background:rgba(0,0,0,.5);\n}\n\n.lightbox-body{\n  position:relative;\n  background:#fff;\n  border-radius:12px;\n  max-width:min(90vw,1000px);\n  max-height:90vh;\n  display:flex;\n  flex-direction:column;\n  gap:10px;\n  padding:12px;\n}\n\n.lightbox-body img{\n  max-width:82vw;\n  max-height:70vh;\n  object-fit:contain;\n  border-radius:8px;\n}\n\n.lb-close{\n  position:absolute;\n  top:6px;\n  right:8px;\n  background:#fff;\n  border:1px solid #e5e7eb;\n  border-radius:50%;\n  width:32px;\n  height:32px;\n  cursor:pointer;\n}\n\n@media (max-width:700px){\n  .brand-subtitle{\n    display:none;\n  }\n}\n<\/style><\/div><\/div>\n\n<div class=\"et_pb_code_2 et_pb_code et_pb_module\"><div class=\"et_pb_code_inner\"><script>\n(async function () {\n  const $ = (s, ctx = document) => ctx.querySelector(s);\n\n  function getParam(name) {\n    return new URLSearchParams(window.location.search).get(name);\n  }\n\n  function esc(str) {\n    return String(str ?? '')\n      .replace(\/&\/g, '&amp;')\n      .replace(\/<\/g, '&lt;')\n      .replace(\/>\/g, '&gt;')\n      .replace(\/\"\/g, '&quot;');\n  }\n\n  function normalizeCalloutBase(value) {\n    const raw = String(value ?? '').trim();\n    if (!raw) return '';\n    return raw.split('.')[0];\n  }\n\n  const productId = getParam('spb_product');\n  const lang = getParam('spb_lang') || 'it';\n\n  if (!productId) {\n    const drawing = $('#drawing');\n    if (drawing) drawing.innerHTML = '<p class=\"muted\">Nessun prodotto selezionato.<\/p>';\n    return;\n  }\n\n  const basePath = `\/wp-content\/uploads\/spare-parts\/${productId}`;\n  const productPath = `${basePath}\/product.json`;\n  const componentsPath = `${basePath}\/${lang}\/current\/components.json`;\n  const treePath = `${basePath}\/${lang}\/current\/tree.json`;\n  const drawingsPath = `${basePath}\/shared\/drawings.json`;\n\n  async function fetchJson(url) {\n    const r = await fetch(url, { credentials: 'same-origin' });\n    if (!r.ok) throw new Error(`Errore nel caricamento: ${url}`);\n    return r.json();\n  }\n\n  let productData = null;\n  let componentsData = null;\n  let treeData = null;\n  let drawingsData = null;\n\n  try {\n    [productData, componentsData, treeData, drawingsData] = await Promise.all([\n      fetchJson(productPath),\n      fetchJson(componentsPath),\n      fetchJson(treePath),\n      fetchJson(drawingsPath)\n    ]);\n  } catch (err) {\n    console.error(err);\n    const drawing = $('#drawing');\n    if (drawing) drawing.innerHTML = '<p class=\"muted\">Impossibile caricare i dati del prodotto.<\/p>';\n    return;\n  }\n\n  const components = Array.isArray(componentsData.components) ? componentsData.components : [];\n  const componentsById = new Map(components.map(c => [c.id, c]));\n  const nodeDrawings = drawingsData.node_drawings || {};\n\n  const initialRoots = (treeData?.root?.children && treeData.root.children.length)\n    ? treeData.root.children\n    : (treeData?.root ? [treeData.root] : []);\n\n  function getInitialNodeFromUrl() {\n    return getParam('node');\n  }\n\n  function getInitialCalloutFromUrl() {\n    return normalizeCalloutBase(getParam('callout'));\n  }\n\n  let activeNodeId = getInitialNodeFromUrl() || initialRoots[0]?.id || null;\n  let activeDrawing = null;\n  let activeCalloutBase = getInitialCalloutFromUrl() || '';\n  let viewState = null;\n  let rafSync = null;\n\n  function updateUrlState() {\n    const url = new URL(window.location.href);\n\n    if (productId) url.searchParams.set('spb_product', productId);\n    if (lang) url.searchParams.set('spb_lang', lang);\n\n    if (activeNodeId) url.searchParams.set('node', activeNodeId);\n    else url.searchParams.delete('node');\n\n    if (activeCalloutBase) url.searchParams.set('callout', activeCalloutBase);\n    else url.searchParams.delete('callout');\n\n    window.history.replaceState({}, '', url.toString());\n  }\n\n  function getProductName() {\n    return productData.product_name || productData.name || productId;\n  }\n\n  function getNodePathById(node, id, path = []) {\n    if (!node) return null;\n    const nextPath = [...path, node];\n    if (node.id === id) return nextPath;\n\n    for (const child of (node.children || [])) {\n      const found = getNodePathById(child, id, nextPath);\n      if (found) return found;\n    }\n    return null;\n  }\n\n  function renderBreadcrumb(pathNodes) {\n    const bc = $('#spb-breadcrumb');\n    if (!bc) return;\n\n    const items = (Array.isArray(pathNodes) ? pathNodes : [])\n      .filter((n, i, arr) => !(i === 0 && arr.length > 1 && n.id === treeData?.root?.id))\n      .map(n => n.title || n.id)\n      .filter(Boolean);\n\n    if (!items.length) {\n      bc.innerHTML = '';\n      bc.style.display = 'none';\n      return;\n    }\n\n    bc.innerHTML = items.map((label, i) => {\n      const sep = i < items.length - 1 ? `<span class=\"spb-breadcrumb__sep\">\/<\/span>` : '';\n      return `<span class=\"spb-breadcrumb__item\">${esc(label)}<\/span>${sep}`;\n    }).join('');\n    bc.style.display = 'flex';\n  }\n\n  function setActiveGroup(label) {\n    const el = $('#spb-active-group');\n    if (!el) return;\n\n    if (label) {\n      el.textContent = label;\n      el.style.display = 'block';\n    } else {\n      el.textContent = '';\n      el.style.display = 'none';\n    }\n  }\n\n  function setProductHeader() {\n    const heroTitle = $('#spb-hero-product-title');\n    const heroSubtitle = $('#spb-hero-product-subtitle');\n    const heroDescription = $('#spb-hero-product-description');\n    const heroImage = $('#spb-hero-product-image');\n\n    const productName = getProductName();\n    const productDescription =\n      productData.product_description ||\n      productData.description ||\n      productData.extra_description ||\n      '';\n\n    const rawImage =\n      productData.product_image_url ||\n      productData.image_url ||\n      productData.product_image ||\n      '';\n\n    let productImageUrl = '';\n    if (rawImage) {\n      if (\/^https?:\\\/\\\/\/i.test(rawImage) || rawImage.startsWith('\/')) {\n        productImageUrl = rawImage;\n      } else {\n        productImageUrl = `${basePath}\/${String(rawImage).replace(\/^\\\/+\/, '')}`;\n      }\n    }\n\n    if (heroTitle) heroTitle.textContent = productName;\n    if (heroSubtitle) heroSubtitle.textContent = 'Spare Parts Procurement Page';\n\n    if (heroDescription) {\n      heroDescription.textContent = productDescription;\n      heroDescription.style.display = productDescription ? '' : 'none';\n    }\n\n    if (heroImage) {\n      if (productImageUrl) {\n        heroImage.src = productImageUrl;\n        heroImage.alt = productName;\n        heroImage.style.display = '';\n      } else {\n        heroImage.style.display = 'none';\n      }\n    }\n\n    setActiveGroup('');\n    renderBreadcrumb([]);\n    document.title = `${productName} - Spare Parts`;\n  }\n\n  function setCurrentGroupTitle(label, pathNodes = []) {\n    const heroTitle = $('#spb-hero-product-title');\n    const heroSubtitle = $('#spb-hero-product-subtitle');\n\n    if (heroTitle) heroTitle.textContent = getProductName();\n    if (heroSubtitle) heroSubtitle.textContent = 'Spare Parts Procurement Page';\n\n    setActiveGroup(label || '');\n    renderBreadcrumb(pathNodes);\n  }\n\n  function findNodeById(node, id) {\n    if (!node) return null;\n    if (node.id === id) return node;\n    for (const child of (node.children || [])) {\n      const found = findNodeById(child, id);\n      if (found) return found;\n    }\n    return null;\n  }\n\n  function findNodePathByComponentId(node, componentId, path = []) {\n    if (!node) return null;\n    const nextPath = [...path, node];\n\n    if (Array.isArray(node.component_ids) && node.component_ids.includes(componentId)) {\n      return nextPath;\n    }\n\n    for (const child of (node.children || [])) {\n      const found = findNodePathByComponentId(child, componentId, nextPath);\n      if (found) return found;\n    }\n\n    return null;\n  }\n\n  function collectDescendantComponentIds(node) {\n    const out = new Set();\n    (function walk(n) {\n      (n.component_ids || []).forEach(id => out.add(id));\n      (n.children || []).forEach(walk);\n    })(node);\n    return Array.from(out);\n  }\n\n  function getDrawingsForNode(nodeId) {\n    return Array.isArray(nodeDrawings[nodeId]) ? nodeDrawings[nodeId] : [];\n  }\n\n  function findNearestDrawingNodeIdFromPath(path) {\n    if (!Array.isArray(path) || !path.length) return null;\n    for (let i = path.length - 1; i >= 0; i--) {\n      const nodeId = path[i]?.id;\n      if (nodeId && Array.isArray(nodeDrawings[nodeId]) && nodeDrawings[nodeId].length) return nodeId;\n    }\n    return null;\n  }\n\n  function getAssetUrl(relativePath) {\n    if (!relativePath) return '';\n    return `${basePath}\/${relativePath.replace(\/^\\\/+\/, '')}`;\n  }\n\n  function getHotspotTargetComponent(hs) {\n    if (hs.target_type !== 'component') return null;\n    return componentsById.get(hs.target_id) || null;\n  }\n\n  function getCurrentGroupComponents() {\n    if (!activeNodeId) return components;\n    const node = findNodeById(treeData.root, activeNodeId);\n    if (!node) return components;\n    const ids = collectDescendantComponentIds(node);\n    return ids.map(id => componentsById.get(id)).filter(Boolean);\n  }\n\n  function getCurrentGroupComponentsByCalloutBase(base) {\n    const normalized = normalizeCalloutBase(base);\n    if (!normalized) return [];\n    return getCurrentGroupComponents().filter(c =>\n      normalizeCalloutBase(c.callout_base || c.callout) === normalized\n    );\n  }\n\n  function getComponentsByCalloutBase(base) {\n    return getCurrentGroupComponentsByCalloutBase(base);\n  }\n\n  function getComponentCalloutBase(component) {\n    return normalizeCalloutBase(component?.callout_base || component?.callout || '');\n  }\n\n  function setActiveCalloutBase(base) {\n    activeCalloutBase = normalizeCalloutBase(base);\n    updateUrlState();\n    scheduleSyncActiveStates();\n  }\n\n  function syncActiveStates() {\n    document.querySelectorAll('.dwg-hotspot').forEach(el => {\n      const base = normalizeCalloutBase(el.dataset.calloutBase || el.dataset.label || '');\n      el.classList.toggle('is-active', !!activeCalloutBase && base === activeCalloutBase);\n    });\n\n    document.querySelectorAll('.list-item').forEach(el => {\n      const base = normalizeCalloutBase(el.dataset.calloutBase || '');\n      el.classList.toggle('is-active', !!activeCalloutBase && base === activeCalloutBase);\n    });\n  }\n\n  function scheduleSyncActiveStates() {\n    if (rafSync) cancelAnimationFrame(rafSync);\n    rafSync = requestAnimationFrame(() => {\n      syncActiveStates();\n      rafSync = null;\n    });\n  }\n\n  function getDisplayRoots() {\n    if (!treeData?.root) return [];\n    const root = treeData.root;\n    const children = Array.isArray(root.children) ? root.children : [];\n    return children.length ? children : [root];\n  }\n\n  function scrollActiveNodeIntoView() {\n    const treePanel = document.querySelector('.panel.tree');\n    const activeRow = treePanel?.querySelector('.tree-item.is-active > .tree-row');\n    if (activeRow) activeRow.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n  }\n\n  function renderTree() {\n    const el = $('#tree');\n    if (!el) return;\n\n    const roots = getDisplayRoots();\n    if (!roots.length) {\n      el.innerHTML = '<p class=\"muted\">Nessun albero disponibile.<\/p>';\n      return;\n    }\n\n    const activePath = activeNodeId ? (getNodePathById(treeData.root, activeNodeId) || []) : [];\n    const activeIds = new Set(activePath.map(n => n.id));\n\n    function nodeHTML(node, isRoot = false) {\n      const hasChildren = node.children && node.children.length;\n      const isActive = node.id === activeNodeId;\n      const isInPath = activeIds.has(node.id);\n\n      return `\n        <li class=\"tree-item ${isRoot ? 'tree-root-item' : ''} ${isActive ? 'is-active' : ''} ${isInPath ? 'is-open' : ''}\" data-node=\"${esc(node.id)}\">\n          <div class=\"tree-row\">\n            ${\n              hasChildren\n                ? `<button class=\"tree-toggle\" type=\"button\" aria-label=\"Apri\/chiudi\">${isInPath ? '\u25be' : '\u25b8'}<\/button>`\n                : `<span class=\"tree-leaf-icon\">\u25fb<\/span>`\n            }\n            <button class=\"tree-title\" type=\"button\" data-select=\"${esc(node.id)}\">${esc(node.title || node.id)}<\/button>\n          <\/div>\n          ${\n            hasChildren\n              ? `<ul class=\"tree-item__children\">${node.children.map(child => nodeHTML(child, false)).join('')}<\/ul>`\n              : ''\n          }\n        <\/li>\n      `;\n    }\n\n    el.innerHTML = `<ul class=\"tree-root\">${roots.map(n => nodeHTML(n, true)).join('')}<\/ul>`;\n\n    el.querySelectorAll('.tree-toggle').forEach(btn => {\n      btn.addEventListener('click', (e) => {\n        e.stopPropagation();\n        const item = e.currentTarget.closest('.tree-item');\n        if (!item) return;\n\n        item.classList.toggle('is-open');\n        e.currentTarget.textContent = item.classList.contains('is-open') ? '\u25be' : '\u25b8';\n      });\n    });\n\n    el.querySelectorAll('[data-select]').forEach(btn => {\n      btn.addEventListener('click', () => {\n        activeNodeId = btn.dataset.select;\n        const node = findNodeById(treeData.root, activeNodeId);\n        const drawings = getDrawingsForNode(activeNodeId);\n        const path = getNodePathById(treeData.root, activeNodeId) || [];\n        activeDrawing = drawings[0] || null;\n        viewState = null;\n\n        if (activeDrawing) renderDrawing(activeDrawing);\n        else $('#drawing').innerHTML = '<p class=\"muted\">Nessun drawing disponibile per questo nodo.<\/p>';\n\n        if (node) {\n          const ids = collectDescendantComponentIds(node);\n          renderList(ids.map(id => componentsById.get(id)).filter(Boolean));\n          setCurrentGroupTitle(node.title || '', path);\n        }\n\n        activeCalloutBase = '';\n        updateUrlState();\n        renderTree();\n        scheduleSyncActiveStates();\n        setTimeout(scrollActiveNodeIntoView, 50);\n      });\n    });\n\n    setTimeout(scrollActiveNodeIntoView, 50);\n  }\n\n  function focusComponentInTree(component) {\n    if (!component || !treeData?.root) return;\n\n    const path = findNodePathByComponentId(treeData.root, component.id);\n    if (!path || !path.length) return;\n\n    const drawingNodeId = findNearestDrawingNodeIdFromPath(path);\n    const groupNode = path[path.length - 1];\n\n    if (drawingNodeId) {\n      activeNodeId = drawingNodeId;\n      const drawings = getDrawingsForNode(activeNodeId);\n      activeDrawing = drawings[0] || null;\n    } else {\n      activeNodeId = groupNode.id;\n      activeDrawing = null;\n    }\n\n    viewState = null;\n    renderTree();\n\n    if (activeDrawing) renderDrawing(activeDrawing);\n    else $('#drawing').innerHTML = '<p class=\"muted\">Nessun drawing disponibile per questo gruppo.<\/p>';\n\n    setCurrentGroupTitle(groupNode.title || '', path);\n    updateUrlState();\n    setTimeout(scrollActiveNodeIntoView, 50);\n  }\n\n  function renderDrawing(drawing) {\n    const el = $('#drawing');\n    if (!el) return;\n\n    if (!drawing) {\n      el.innerHTML = '<p class=\"muted\">Nessun drawing disponibile.<\/p>';\n      return;\n    }\n\n    const baseImage = getAssetUrl(drawing.base_image);\n    const hotspots = Array.isArray(drawing.hotspots) ? drawing.hotspots : [];\n    const fontSize = Number(drawing.label_font_size || 14);\n    const aspectRatio = drawing.aspect_ratio || '16 \/ 9';\n\n    const VIEW_W = 1000;\n    const VIEW_H = 1000;\n    const VIEW_PAD = 55;\n\n    el.innerHTML = `\n      <div class=\"dwg-toolbar\">\n        <button type=\"button\" id=\"dwg-zoom-in\">+<\/button>\n        <button type=\"button\" id=\"dwg-zoom-out\">\u2212<\/button>\n        <button type=\"button\" id=\"dwg-zoom-reset\">Reset Zoom<\/button>\n      <\/div>\n      <div class=\"dwg-wrap\" id=\"dwg-wrap\">\n        <div class=\"dwg-stage\" id=\"dwg-stage\" style=\"aspect-ratio:${aspectRatio}\"><\/div>\n      <\/div>\n    `;\n\n    const stage = $('#dwg-stage', el);\n    const wrap = $('#dwg-wrap', el);\n\n    function hotspotRadius() {\n      return Math.max(12, fontSize * 1.2);\n    }\n\n    function makeViewBox(scale, panX, panY) {\n      const fullW = VIEW_W + VIEW_PAD * 2;\n      const fullH = VIEW_H + VIEW_PAD * 2;\n      const baseX = -VIEW_PAD;\n      const baseY = -VIEW_PAD;\n\n      const w = fullW \/ scale;\n      const h = fullH \/ scale;\n\n      const maxPanX = Math.max(0, fullW - w);\n      const maxPanY = Math.max(0, fullH - h);\n\n      const clampedPanX = Math.max(0, Math.min(maxPanX, panX));\n      const clampedPanY = Math.max(0, Math.min(maxPanY, panY));\n\n      return {\n        x: baseX + clampedPanX,\n        y: baseY + clampedPanY,\n        w,\n        h,\n        panX: clampedPanX,\n        panY: clampedPanY,\n        fullW,\n        fullH\n      };\n    }\n\n    function ensureViewState() {\n      if (!viewState) viewState = { scale: 1, panX: 0, panY: 0 };\n      viewState.scale = Math.max(1, Math.min(4, viewState.scale || 1));\n      const vb = makeViewBox(viewState.scale, viewState.panX || 0, viewState.panY || 0);\n      viewState.panX = vb.panX;\n      viewState.panY = vb.panY;\n      return vb;\n    }\n\n    function buildScene() {\n      const vb = ensureViewState();\n\n      const svg = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'svg');\n      svg.setAttribute('class', 'dwg-scene');\n      svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);\n      svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');\n\n      if (baseImage) {\n        const img = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'image');\n        img.setAttribute('class', 'dwg-scene-image');\n        img.setAttributeNS('http:\/\/www.w3.org\/1999\/xlink', 'href', baseImage);\n        img.setAttribute('href', baseImage);\n        img.setAttribute('x', '0');\n        img.setAttribute('y', '0');\n        img.setAttribute('width', String(VIEW_W));\n        img.setAttribute('height', String(VIEW_H));\n        img.setAttribute('preserveAspectRatio', 'xMidYMid meet');\n        svg.appendChild(img);\n      }\n\n      const connectorsLayer = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'g');\n      svg.appendChild(connectorsLayer);\n\n      hotspots.forEach(hs => {\n        const x1 = (Number(hs.x || 0) \/ 100) * VIEW_W;\n        const y1 = (Number(hs.y || 0) \/ 100) * VIEW_H;\n        const cxp = typeof hs.connector_x === 'number' ? hs.connector_x : hs.x;\n        const cyp = typeof hs.connector_y === 'number' ? hs.connector_y : hs.y;\n        const x2 = (Number(cxp || 0) \/ 100) * VIEW_W;\n        const y2 = (Number(cyp || 0) \/ 100) * VIEW_H;\n\n        if (Math.abs(x1 - x2) < 0.01 && Math.abs(y1 - y2) < 0.01) return;\n\n        const line = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'line');\n        line.setAttribute('class', 'dwg-connector-line');\n        line.setAttribute('x1', String(x1));\n        line.setAttribute('y1', String(y1));\n        line.setAttribute('x2', String(x2));\n        line.setAttribute('y2', String(y2));\n        connectorsLayer.appendChild(line);\n      });\n\n      const hotspotsLayer = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'g');\n      svg.appendChild(hotspotsLayer);\n\n      hotspots.forEach(hs => {\n        const label = String(hs.label || '');\n        const x = (Number(hs.x || 0) \/ 100) * VIEW_W;\n        const y = (Number(hs.y || 0) \/ 100) * VIEW_H;\n        const base = normalizeCalloutBase(label);\n        const r = hotspotRadius();\n\n        const g = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'g');\n        g.setAttribute('class', 'dwg-hotspot');\n        g.dataset.label = label;\n        g.dataset.calloutBase = base;\n\n        const circle = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'circle');\n        circle.setAttribute('class', 'dwg-hotspot-circle');\n        circle.setAttribute('cx', String(x));\n        circle.setAttribute('cy', String(y));\n        circle.setAttribute('r', String(r));\n        g.appendChild(circle);\n\n        const text = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'text');\n        text.setAttribute('class', 'dwg-hotspot-text');\n        text.setAttribute('x', String(x));\n        text.setAttribute('y', String(y));\n        text.setAttribute('font-size', String(fontSize));\n        text.textContent = label;\n        g.appendChild(text);\n\n        const hit = document.createElementNS('http:\/\/www.w3.org\/2000\/svg', 'circle');\n        hit.setAttribute('class', 'dwg-hotspot-hit');\n        hit.setAttribute('cx', String(x));\n        hit.setAttribute('cy', String(y));\n        hit.setAttribute('r', String(Math.max(r * 1.5, 20)));\n        g.appendChild(hit);\n\n        g.addEventListener('mouseenter', () => g.classList.add('is-hovered'));\n        g.addEventListener('mouseleave', () => g.classList.remove('is-hovered'));\n\n        g.addEventListener('click', () => {\n          if (hs.target_type === 'node') {\n            activeNodeId = hs.target_id;\n            const node = findNodeById(treeData.root, activeNodeId);\n            const drawings = getDrawingsForNode(activeNodeId);\n            const path = getNodePathById(treeData.root, activeNodeId) || [];\n            activeDrawing = drawings[0] || null;\n            viewState = null;\n\n            if (activeDrawing) renderDrawing(activeDrawing);\n            else $('#drawing').innerHTML = '<p class=\"muted\">Nessun drawing disponibile per questo nodo.<\/p>';\n\n            if (node) {\n              const ids = collectDescendantComponentIds(node);\n              renderList(ids.map(id => componentsById.get(id)).filter(Boolean));\n              setCurrentGroupTitle(node.title || '', path);\n            }\n\n            activeCalloutBase = '';\n            updateUrlState();\n            renderTree();\n            scheduleSyncActiveStates();\n            setTimeout(scrollActiveNodeIntoView, 50);\n            return;\n          }\n\n          if (hs.target_type === 'component') {\n            const comp = getHotspotTargetComponent(hs);\n\n            if (comp) {\n              const baseVal = normalizeCalloutBase(comp.callout_base || comp.callout || hs.label);\n              const grouped = getComponentsByCalloutBase(baseVal);\n              renderList(grouped.length ? grouped : [comp]);\n              setActiveCalloutBase(baseVal);\n              focusComponentInTree(comp);\n            } else {\n              const baseVal = normalizeCalloutBase(hs.label);\n              const grouped = getComponentsByCalloutBase(baseVal);\n              if (grouped.length) {\n                renderList(grouped);\n                setActiveCalloutBase(baseVal);\n              }\n            }\n          }\n        });\n\n        hotspotsLayer.appendChild(g);\n      });\n\n      stage.innerHTML = '';\n      stage.appendChild(svg);\n      syncSceneActiveStates();\n    }\n\n    function syncSceneActiveStates() {\n      stage.querySelectorAll('.dwg-hotspot').forEach(g => {\n        const base = normalizeCalloutBase(g.dataset.calloutBase || g.dataset.label || '');\n        g.classList.toggle('is-active', !!activeCalloutBase && base === activeCalloutBase);\n      });\n    }\n\n    function rerender() {\n      buildScene();\n      scheduleSyncActiveStates();\n    }\n\n    function zoomAt(clientX, clientY, deltaScale) {\n      const svg = stage.querySelector('svg');\n      const rect = svg.getBoundingClientRect();\n      const vb = svg.viewBox.baseVal;\n      const mouseSvgX = vb.x + ((clientX - rect.left) \/ rect.width) * vb.width;\n      const mouseSvgY = vb.y + ((clientY - rect.top) \/ rect.height) * vb.height;\n\n      const nextScale = Math.max(1, Math.min(4, +(viewState.scale + deltaScale).toFixed(2)));\n      if (nextScale === viewState.scale) return;\n\n      const fullW = VIEW_W + VIEW_PAD * 2;\n      const fullH = VIEW_H + VIEW_PAD * 2;\n      const newW = fullW \/ nextScale;\n      const newH = fullH \/ nextScale;\n\n      const relX = (mouseSvgX - vb.x) \/ vb.width;\n      const relY = (mouseSvgY - vb.y) \/ vb.height;\n\n      const newX = mouseSvgX - relX * newW;\n      const newY = mouseSvgY - relY * newH;\n\n      const baseX = -VIEW_PAD;\n      const baseY = -VIEW_PAD;\n\n      viewState.scale = nextScale;\n      viewState.panX = newX - baseX;\n      viewState.panY = newY - baseY;\n\n      const clamped = makeViewBox(viewState.scale, viewState.panX, viewState.panY);\n      viewState.panX = clamped.panX;\n      viewState.panY = clamped.panY;\n\n      rerender();\n    }\n\n    function resetZoom() {\n      viewState = { scale: 1, panX: 0, panY: 0 };\n      rerender();\n    }\n\n    viewState = viewState || { scale: 1, panX: 0, panY: 0 };\n    rerender();\n\n    $('#dwg-zoom-in', el)?.addEventListener('click', () => {\n      const rect = stage.getBoundingClientRect();\n      zoomAt(rect.left + rect.width \/ 2, rect.top + rect.height \/ 2, 0.2);\n    });\n\n    $('#dwg-zoom-out', el)?.addEventListener('click', () => {\n      const rect = stage.getBoundingClientRect();\n      zoomAt(rect.left + rect.width \/ 2, rect.top + rect.height \/ 2, -0.2);\n    });\n\n    $('#dwg-zoom-reset', el)?.addEventListener('click', resetZoom);\n\n    wrap?.addEventListener('wheel', (e) => {\n      e.preventDefault();\n      const delta = e.deltaY > 0 ? -0.1 : 0.1;\n      const nextScale = Math.max(1, Math.min(4, +(viewState.scale + delta).toFixed(2)));\n      if (nextScale === viewState.scale) return;\n      zoomAt(e.clientX, e.clientY, delta);\n    }, { passive: false });\n\n    let isPanning = false;\n    let panStart = null;\n\n    wrap?.addEventListener('mousedown', (e) => {\n      const isHotspot = e.target.closest('.dwg-hotspot');\n      if (isHotspot) return;\n\n      isPanning = true;\n      panStart = {\n        x: e.clientX,\n        y: e.clientY,\n        panX: viewState.panX,\n        panY: viewState.panY\n      };\n      e.preventDefault();\n    });\n\n    window.addEventListener('mousemove', (e) => {\n      if (!isPanning || !panStart || viewState.scale <= 1) return;\n\n      const svg = stage.querySelector('svg');\n      const rect = svg.getBoundingClientRect();\n      const vb = svg.viewBox.baseVal;\n\n      const dxSvg = ((e.clientX - panStart.x) \/ rect.width) * vb.width;\n      const dySvg = ((e.clientY - panStart.y) \/ rect.height) * vb.height;\n\n      viewState.panX = panStart.panX - dxSvg;\n      viewState.panY = panStart.panY - dySvg;\n\n      const clamped = makeViewBox(viewState.scale, viewState.panX, viewState.panY);\n      viewState.panX = clamped.panX;\n      viewState.panY = clamped.panY;\n\n      rerender();\n    });\n\n    window.addEventListener('mouseup', () => {\n      isPanning = false;\n      panStart = null;\n    });\n  }\n\n  function openLightbox(src, caption) {\n    const lb = $('#lightbox');\n    const img = $('#lb-img');\n    const cap = $('#lb-caption');\n    if (!lb || !img || !cap) return;\n\n    img.src = src;\n    cap.textContent = caption || '';\n    lb.hidden = false;\n\n    function close() {\n      lb.hidden = true;\n      img.src = '';\n      cap.textContent = '';\n      document.removeEventListener('keydown', onKey);\n    }\n\n    function onKey(e) {\n      if (e.key === 'Escape') close();\n    }\n\n    $('.lb-close')?.addEventListener('click', close, { once: true });\n    $('.lightbox-backdrop')?.addEventListener('click', close, { once: true });\n    document.addEventListener('keydown', onKey);\n  }\n\n  const cartKey = `spb_cart_${productId}`;\n  const cart = {\n    lines: JSON.parse(localStorage.getItem(cartKey) || '[]'),\n    save() {\n      localStorage.setItem(cartKey, JSON.stringify(this.lines));\n      renderCart();\n      updateCartBadge();\n    },\n    add(id, qty = 1) {\n      const found = this.lines.find(l => l.id === id);\n      if (found) found.qty += qty;\n      else this.lines.push({ id, qty });\n      this.save();\n    },\n    update(id, qty) {\n      const it = this.lines.find(l => l.id === id);\n      if (it) {\n        it.qty = Math.max(1, parseInt(qty || 1, 10));\n        this.save();\n      }\n    },\n    remove(id) {\n      this.lines = this.lines.filter(l => l.id !== id);\n      this.save();\n    }\n  };\n\n  function updateCartBadge() {\n    const el = $('#cart-count');\n    if (!el) return;\n    const total = cart.lines.reduce((sum, line) => sum + (parseInt(line.qty, 10) || 0), 0);\n    el.textContent = String(total);\n  }\n\n  function renderCart() {\n    const el = $('#cart');\n    if (!el) return;\n\n    if (!cart.lines.length) {\n      el.innerHTML = '<p class=\"muted\">Carrello vuoto<\/p>';\n      return;\n    }\n\n    el.innerHTML = cart.lines.map(line => {\n      const c = componentsById.get(line.id);\n      return `\n        <div class=\"cart-line\">\n          <div>${esc(c?.code || line.id)} \u2014 ${esc(c?.description || '')}<\/div>\n          <input type=\"number\" min=\"1\" value=\"${line.qty}\" data-cid=\"${esc(line.id)}\">\n          <button type=\"button\" data-rm=\"${esc(line.id)}\">Rimuovi<\/button>\n        <\/div>\n      `;\n    }).join('') + `<div class=\"total\"><span>Totale righe<\/span><span>${cart.lines.length}<\/span><\/div>`;\n\n    el.onclick = (e) => {\n      const rm = e.target.closest('[data-rm]');\n      if (rm) cart.remove(rm.dataset.rm);\n    };\n\n    el.oninput = (e) => {\n      const inp = e.target.closest('input[data-cid]');\n      if (inp) cart.update(inp.dataset.cid, parseInt(inp.value || '1', 10));\n    };\n  }\n\n  function renderList(items) {\n    const el = $('#list');\n    if (!el) return;\n\n    if (!items || !items.length) {\n      el.innerHTML = '<p class=\"muted\">Nessun componente<\/p>';\n      return;\n    }\n\n    el.innerHTML = items.map(c => `\n      <div class=\"list-item\" data-callout-base=\"${esc(getComponentCalloutBase(c))}\" data-component-id=\"${esc(c.id)}\">\n        <div class=\"pos\">\n          <div class=\"pos-label\">Pos.<\/div>\n          <div class=\"pos-num\">${esc(c.callout || c.callout_base || '-')}<\/div>\n        <\/div>\n\n        <div class=\"meta\">\n          <div class=\"code-badge\">${esc(c.code || '-')}<\/div>\n          <div class=\"title\">\n            <strong>${esc(c.description || '')}<\/strong>\n            ${(c.media && c.media.length)\n              ? `<button class=\"img-btn\" type=\"button\" data-img=\"${esc(c.media[0].href || c.media[0].url)}\" data-title=\"${esc((c.code || '') + ' \u2014 ' + (c.description || ''))}\">\ud83d\udcf7<\/button>`\n              : ''\n            }\n          <\/div>\n          <div class=\"group muted\">${esc(c.group || '')}<\/div>\n        <\/div>\n\n        <div class=\"qty\">\n          <input type=\"number\" min=\"1\" value=\"1\" data-id=\"${esc(c.id)}\">\n          <button type=\"button\" data-add=\"${esc(c.id)}\" class=\"btn-ghost\">Aggiungi<\/button>\n        <\/div>\n      <\/div>\n    `).join('');\n\n    el.onclick = (e) => {\n      const imgBtn = e.target.closest('.img-btn');\n      if (imgBtn) {\n        openLightbox(imgBtn.dataset.img, imgBtn.dataset.title);\n        return;\n      }\n\n      const add = e.target.closest('button[data-add]');\n      if (add) {\n        const id = add.dataset.add;\n        const qty = parseInt(el.querySelector(`input[data-id=\"${CSS.escape(id)}\"]`)?.value || '1', 10);\n        cart.add(id, qty);\n      }\n    };\n\n    el.querySelectorAll('.list-item').forEach(item => {\n      item.addEventListener('mouseenter', () => {\n        const base = normalizeCalloutBase(item.dataset.calloutBase || '');\n        document.querySelectorAll('.dwg-hotspot').forEach(hs => {\n          const hsBase = normalizeCalloutBase(hs.dataset.calloutBase || '');\n          hs.classList.toggle('is-hovered', !!base && hsBase === base);\n        });\n        item.classList.add('is-hovered');\n      });\n\n      item.addEventListener('mouseleave', () => {\n        document.querySelectorAll('.dwg-hotspot').forEach(hs => hs.classList.remove('is-hovered'));\n        item.classList.remove('is-hovered');\n      });\n\n      item.addEventListener('click', () => {\n        const base = normalizeCalloutBase(item.dataset.calloutBase || '');\n        setActiveCalloutBase(base);\n\n        const componentId = item.dataset.componentId;\n        const component = componentsById.get(componentId);\n        if (component) focusComponentInTree(component);\n      });\n    });\n\n    scheduleSyncActiveStates();\n  }\n\n  function search(term) {\n    term = (term || '').trim().toLowerCase();\n    if (!term) return [];\n\n    return components.filter(c =>\n      [c.code, c.description, c.callout, c.callout_base, c.group]\n        .filter(Boolean)\n        .some(v => String(v).toLowerCase().includes(term))\n    ).slice(0, 20);\n  }\n\n  const q = $('#q');\n  const sug = $('#suggestions');\n  let activeIndex = -1;\n\n  function updateActiveSuggestion() {\n    const items = sug.querySelectorAll('.sug');\n    items.forEach((el, i) => {\n      el.classList.toggle('active', i === activeIndex);\n      if (i === activeIndex) el.scrollIntoView({ block: 'nearest' });\n    });\n  }\n\n  function selectSuggestion(index) {\n    const items = sug.querySelectorAll('.sug');\n    const el = items[index];\n    if (!el) return;\n\n    const id = el.dataset.id;\n    const c = componentsById.get(id);\n\n    if (c) {\n      const base = getComponentCalloutBase(c);\n      const grouped = getComponentsByCalloutBase(base);\n      renderList(grouped.length ? grouped : [c]);\n      setActiveCalloutBase(base);\n      focusComponentInTree(c);\n    }\n\n    sug.style.display = 'none';\n    activeIndex = -1;\n  }\n\n  q?.addEventListener('input', () => {\n    const term = q.value.trim();\n    const items = search(term);\n\n    if (!term || !items.length) {\n      sug.style.display = 'none';\n      return;\n    }\n\n    sug.style.display = 'block';\n    sug.innerHTML = items.map(c => `\n      <div class=\"sug\" data-id=\"${esc(c.id)}\">\n        <b>${esc(c.code || '-')}<\/b> \u2013 Pos. ${esc(c.callout || c.callout_base || '-')} \u2014 ${esc(c.description || '')}\n      <\/div>\n    `).join('');\n\n    activeIndex = -1;\n  });\n\n  q?.addEventListener('keydown', (e) => {\n    const items = sug.querySelectorAll('.sug');\n    if (!items.length || sug.style.display === 'none') return;\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault();\n        activeIndex = (activeIndex + 1) % items.length;\n        updateActiveSuggestion();\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        activeIndex = (activeIndex - 1 + items.length) % items.length;\n        updateActiveSuggestion();\n        break;\n      case 'Enter':\n        e.preventDefault();\n        if (activeIndex >= 0) selectSuggestion(activeIndex);\n        break;\n      case 'Escape':\n        sug.style.display = 'none';\n        activeIndex = -1;\n        break;\n    }\n  });\n\n  sug?.addEventListener('click', (e) => {\n    const it = e.target.closest('[data-id]');\n    if (!it) return;\n\n    const id = it.dataset.id;\n    const c = componentsById.get(id);\n\n    if (c) {\n      const base = getComponentCalloutBase(c);\n      const grouped = getComponentsByCalloutBase(base);\n      renderList(grouped.length ? grouped : [c]);\n      setActiveCalloutBase(base);\n      focusComponentInTree(c);\n    }\n\n    sug.style.display = 'none';\n    activeIndex = -1;\n  });\n\n  document.addEventListener('click', (e) => {\n    if (sug && !sug.contains(e.target) && e.target !== q) {\n      sug.style.display = 'none';\n      activeIndex = -1;\n    }\n  });\n\n  const cartToggle = $('#cart-toggle');\n  const cartPanel = $('#cart-panel');\n\n  function setCartOpen(open) {\n    if (!cartPanel || !cartToggle) return;\n    cartPanel.hidden = !open;\n    cartToggle.setAttribute('aria-expanded', String(open));\n  }\n\n  cartToggle?.addEventListener('click', () => setCartOpen(cartPanel.hidden));\n\n  document.addEventListener('click', (e) => {\n    if (!cartPanel || !cartToggle) return;\n    if (!cartPanel.contains(e.target) && !cartToggle.contains(e.target)) {\n      setCartOpen(false);\n    }\n  });\n\n  $('#checkout')?.addEventListener('click', () => {\n    const payload = {\n      product_id: productId,\n      product_name: getProductName(),\n      lines: cart.lines.map(line => {\n        const c = componentsById.get(line.id);\n        return {\n          id: line.id,\n          code: c?.code || '',\n          callout_base: c?.callout_base || '',\n          callout: c?.callout || '',\n          description: c?.description || '',\n          qty: line.qty\n        };\n      })\n    };\n\n    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application\/json' });\n    const a = document.createElement('a');\n    a.href = URL.createObjectURL(blob);\n    a.download = `ordine-${productId}.json`;\n    a.click();\n    setTimeout(() => URL.revokeObjectURL(a.href), 0);\n  });\n\n  setProductHeader();\n  renderTree();\n\n  const initialNode = activeNodeId ? findNodeById(treeData.root, activeNodeId) : null;\n  const initialPath = activeNodeId ? getNodePathById(treeData.root, activeNodeId) || [] : [];\n  const rootDrawings = activeNodeId ? getDrawingsForNode(activeNodeId) : [];\n  activeDrawing = rootDrawings[0] || null;\n\n  if (initialNode) {\n    setCurrentGroupTitle(initialNode.title || '', initialPath);\n    const ids = collectDescendantComponentIds(initialNode);\n    let initialItems = ids.map(id => componentsById.get(id)).filter(Boolean);\n\n    if (activeCalloutBase) {\n      const filtered = initialItems.filter(c => getComponentCalloutBase(c) === activeCalloutBase);\n      if (filtered.length) initialItems = filtered;\n    }\n\n    renderList(initialItems);\n  } else {\n    renderList([]);\n  }\n\n  if (activeDrawing) renderDrawing(activeDrawing);\n  else $('#drawing').innerHTML = '<p class=\"muted\">Nessun drawing disponibile.<\/p>';\n\n  updateUrlState();\n  scheduleSyncActiveStates();\n  renderCart();\n  updateCartBadge();\n  setTimeout(scrollActiveNodeIntoView, 60);\n})();\n<\/script><\/div><\/div>\n<\/div>\n<\/div>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-32361","page","type-page","status-publish","hentry"],"acf":[],"dsm_author":{"name":"admeb26","avatar_url":"https:\/\/www.meber.it\/wp-content\/plugins\/ultimate-member\/assets\/img\/default_avatar.jpg","archive_link":"https:\/\/www.meber.it\/en\/author\/admeb26\/","biodata":""},"dsm_categories":[],"dsm_attachment_categories":[],"dsm_featured_image":null,"dsm_comment_count":0,"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.9 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Spare Parts View - Me.Ber.<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.meber.it\/en\/spare-parts-view\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Spare Parts View - Me.Ber.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.meber.it\/en\/spare-parts-view\/\" \/>\n<meta property=\"og:site_name\" content=\"Me.Ber.\" \/>\n<meta property=\"article:modified_time\" content=\"2026-05-13T07:22:37+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.meber.it\\\/en\\\/spare-parts-view\\\/\",\"url\":\"https:\\\/\\\/www.meber.it\\\/en\\\/spare-parts-view\\\/\",\"name\":\"Spare Parts View - Me.Ber.\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.meber.it\\\/en\\\/#website\"},\"datePublished\":\"2026-04-07T08:54:31+00:00\",\"dateModified\":\"2026-05-13T07:22:37+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.meber.it\\\/en\\\/spare-parts-view\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.meber.it\\\/en\\\/spare-parts-view\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.meber.it\\\/en\\\/spare-parts-view\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.meber.it\\\/en\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Spare Parts View\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.meber.it\\\/en\\\/#website\",\"url\":\"https:\\\/\\\/www.meber.it\\\/en\\\/\",\"name\":\"Me.Ber.\",\"description\":\"In Case of Emergerncy\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.meber.it\\\/en\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Spare Parts View - Me.Ber.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.meber.it\/en\/spare-parts-view\/","og_locale":"en_US","og_type":"article","og_title":"Spare Parts View - Me.Ber.","og_url":"https:\/\/www.meber.it\/en\/spare-parts-view\/","og_site_name":"Me.Ber.","article_modified_time":"2026-05-13T07:22:37+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.meber.it\/en\/spare-parts-view\/","url":"https:\/\/www.meber.it\/en\/spare-parts-view\/","name":"Spare Parts View - Me.Ber.","isPartOf":{"@id":"https:\/\/www.meber.it\/en\/#website"},"datePublished":"2026-04-07T08:54:31+00:00","dateModified":"2026-05-13T07:22:37+00:00","breadcrumb":{"@id":"https:\/\/www.meber.it\/en\/spare-parts-view\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.meber.it\/en\/spare-parts-view\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.meber.it\/en\/spare-parts-view\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.meber.it\/en\/"},{"@type":"ListItem","position":2,"name":"Spare Parts View"}]},{"@type":"WebSite","@id":"https:\/\/www.meber.it\/en\/#website","url":"https:\/\/www.meber.it\/en\/","name":"Me.Ber.","description":"In Case of Emergerncy","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.meber.it\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"}]}},"_links":{"self":[{"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/pages\/32361","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/comments?post=32361"}],"version-history":[{"count":3,"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/pages\/32361\/revisions"}],"predecessor-version":[{"id":33185,"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/pages\/32361\/revisions\/33185"}],"wp:attachment":[{"href":"https:\/\/www.meber.it\/en\/wp-json\/wp\/v2\/media?parent=32361"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}