@reqlan/analytical 0.5.0 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,6 +30,36 @@ npm install @reqlan/analytical
30
30
 
31
31
  ## Changelog
32
32
 
33
+ ### 0.5.4
34
+
35
+ #### Patch Changes
36
+
37
+ - 3e940a2: cicd trigger.
38
+ - Updated dependencies [3e940a2]
39
+ - @reqlan/language@1.5.3
40
+
41
+ ### 0.5.3
42
+
43
+ #### Patch Changes
44
+
45
+ - 9586a97: package publish trigger
46
+ - Updated dependencies [9586a97]
47
+ - @reqlan/language@1.5.2
48
+
49
+ ### 0.5.2
50
+
51
+ #### Patch Changes
52
+
53
+ - b905cd0: update npm deployement.
54
+ - Updated dependencies [b905cd0]
55
+ - @reqlan/language@1.5.1
56
+
57
+ ### 0.5.1
58
+
59
+ #### Patch Changes
60
+
61
+ - 4f86d2b: fix the graph rendering.
62
+
33
63
  ### 0.5.0
34
64
 
35
65
  #### Minor Changes
@@ -1,3 +1,3 @@
1
1
  export declare const SHARED_STYLES = "\n:root {\n color-scheme: dark;\n --bg: #14100e;\n --bg-elev: #1f1815;\n --bg-soft: #1f1815;\n --bg-raised: #2a211d;\n --fg: #ebe4de;\n --muted: #a89488;\n --faint: #6e5f56;\n --line: #3d2f28;\n --accent: #07a0e5;\n --accent-strong: #0bbefb;\n --accent-dim: #0371c1;\n --rust: #b85c38;\n --rust-muted: #8a4530;\n --good: #4ade80;\n --warn: #fbbf24;\n --bad: #fb7185;\n font-family: Inter, system-ui, sans-serif;\n}\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--fg);\n line-height: 1.55;\n}\na { color: var(--accent-strong); text-decoration: none; }\na:hover { color: var(--accent); }\nmain.layout {\n max-width: 1360px; margin: 0 auto; padding: 1.5rem;\n display: flex; flex-direction: column; gap: 1.5rem;\n}\n.topbar {\n position: sticky; top: 0; z-index: 10;\n background: var(--bg-elev); border-bottom: 1px solid var(--line); margin: 0 -1.5rem;\n padding: 0.9rem 1.5rem;\n}\n.topbar-inner, .hero, .split, .grid, .detail-grid { display: grid; gap: 1.5rem; }\n.topbar-inner { grid-template-columns: 1.3fr auto; align-items: center; }\n.nav { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }\n.nav a {\n padding: 0.45rem 0.75rem; border-radius: 8px;\n background: var(--bg-raised); color: var(--muted); border: 1px solid var(--line);\n}\n.nav a:hover { color: var(--fg); border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }\n.nav a.active {\n background: color-mix(in srgb, var(--rust) 28%, var(--bg-raised));\n color: var(--fg); border-color: var(--rust);\n}\n.nav a.brand-link {\n background: transparent;\n border-color: transparent;\n color: var(--fg);\n font-weight: 650;\n letter-spacing: 0.02em;\n padding-left: 0.15rem;\n padding-right: 0.9rem;\n}\n.nav a.brand-link:hover {\n color: var(--accent);\n border-color: transparent;\n background: transparent;\n}\n.hero { grid-template-columns: 2fr 1fr; }\n.grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));\n gap: 1.5rem;\n row-gap: 1.5rem;\n column-gap: 1.5rem;\n}\n.split, .detail-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));\n gap: 1.5rem;\n}\n.panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result {\n background: var(--bg-raised);\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n.panel, .card, .table-shell, .graph-shell, .print-card, .search-result { padding: 1rem 1.1rem; }\n.metric { padding: 0; overflow: hidden; }\n.metric-link, .metric > span, .metric > strong {\n display: flex; flex-direction: column; gap: 0.35rem; padding: 1rem; color: inherit;\n}\n.metric-link:hover { background: color-mix(in srgb, var(--accent) 10%, var(--bg-raised)); }\n.metric-label, .subtle, .eyebrow, .breadcrumbs { color: var(--muted); }\n.metric-value { font-size: 1.75rem; color: var(--accent-strong); }\n.eyebrow { text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.75rem; color: var(--rust); }\n.page-header { display: grid; gap: 0.55rem; }\n.page-header h1 { margin: 0; }\n.page-header .eyebrow { margin: 0; }\n.page-header .subtle { margin: 0; }\n.toolbar {\n display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; justify-content: space-between;\n margin-bottom: 0.9rem;\n}\n.toolbar .actions, .pill-row, .breadcrumbs { display: flex; gap: 0.75rem; row-gap: 0.85rem; flex-wrap: wrap; align-items: center; }\n.chip, .pill {\n display: inline-flex; align-items: center; gap: 0.35rem;\n padding: 0.28rem 0.6rem; border-radius: 8px;\n border: 1px solid var(--line); background: var(--bg); color: var(--muted);\n}\na.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }\n.graph-controls-bar {\n display: flex;\n flex-wrap: wrap;\n gap: 0.65rem;\n align-items: center;\n margin-bottom: 0.9rem;\n padding: 0.8rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n background: var(--bg);\n}\n.graph-shell > .pill-row { margin-bottom: 0.9rem; }\n.graph-filter {\n flex: 1 1 180px;\n min-width: 160px;\n}\n.graph-action {\n border: 1px solid var(--line);\n background: var(--bg-soft);\n color: var(--fg);\n border-radius: 8px;\n padding: 0.7rem 0.9rem;\n cursor: pointer;\n}\n.graph-action:hover { border-color: var(--accent-dim); color: var(--accent-strong); }\n.graph-action.is-active {\n background: color-mix(in srgb, var(--rust) 28%, var(--bg-soft));\n border-color: var(--rust);\n color: var(--fg);\n}\n.graph-status {\n color: var(--muted);\n font-size: 0.92rem;\n margin-left: auto;\n}\n.searchbar, select, input[type=\"search\"] {\n width: min(100%, 440px); background: var(--bg); color: var(--fg);\n border: 1px solid var(--line); border-radius: 8px; padding: 0.75rem 0.9rem;\n}\n.searchbar:focus, select:focus, input[type=\"search\"]:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);\n}\ntable { width: 100%; border-collapse: collapse; }\nth, td {\n padding: 0.75rem 0.65rem; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top;\n}\nth { color: var(--muted); font-size: 0.94rem; }\ntbody tr:hover { background: var(--bg-raised); }\n.table-shell { overflow-x: auto; }\n.scroll-window {\n --scroll-window-max: min(28rem, 50vh);\n}\nbody[data-runtime-mode=\"interactive\"] .scroll-window {\n max-height: var(--scroll-window-max);\n overflow: auto;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--bg);\n}\nbody[data-runtime-mode=\"interactive\"] .scroll-window > table {\n margin: 0;\n}\nbody[data-runtime-mode=\"interactive\"] .scroll-window .entity-list,\nbody[data-runtime-mode=\"interactive\"] .scroll-window .rollup-list {\n padding: 0.75rem;\n}\n.sortable-th { padding: 0.45rem 0.4rem; }\n.sort-button {\n display: inline-flex; align-items: center; gap: 0.35rem;\n width: 100%; margin: 0; padding: 0.3rem 0.25rem;\n border: 0; background: transparent; color: inherit;\n font: inherit; text-align: left; cursor: pointer;\n}\n.sort-button:hover { color: var(--fg); }\n.sort-button.sort-active { color: var(--accent-strong); }\n.sort-indicator { color: var(--faint); font-size: 0.8em; min-width: 1em; }\n.sort-button.sort-active .sort-indicator { color: var(--accent-strong); }\n.table-filter-toggle {\n display: inline-flex; align-items: center; justify-content: center;\n width: 2.25rem; height: 2.25rem; padding: 0;\n border: 1px solid var(--line); border-radius: 8px;\n background: var(--bg-raised); color: var(--muted); cursor: pointer;\n}\n.table-filter-toggle:hover { color: var(--fg); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }\n.table-filter-toggle.is-active {\n color: var(--fg);\n border-color: var(--accent);\n background: color-mix(in srgb, var(--accent) 18%, var(--bg-raised));\n}\n.table-filter-toggle.has-filters:not(.is-active) {\n color: var(--accent-strong);\n border-color: color-mix(in srgb, var(--accent) 45%, var(--line));\n}\n.table-filter-toggle svg { width: 1rem; height: 1rem; display: block; }\n.table-filter-actions {\n display: flex; justify-content: flex-end; gap: 0.55rem;\n margin-bottom: 0.65rem;\n}\n.column-filter-row { display: none; }\n.column-filter-row.is-open { display: table-row; }\n.column-filter-row th {\n padding: 0.35rem 0.4rem 0.75rem; border-bottom: 1px solid var(--line); font-weight: 400;\n}\n.column-filter {\n width: 100%; min-width: 4.5rem; background: var(--bg); color: var(--fg);\n border: 1px solid var(--line); border-radius: 6px; padding: 0.4rem 0.55rem; font-size: 0.85rem;\n}\n.column-filter:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);\n}\nbody[data-runtime-mode=\"interactive\"] .scroll-window thead tr:first-child th {\n position: sticky;\n top: 0;\n z-index: 2;\n background: var(--bg-raised);\n box-shadow: 0 1px 0 var(--line);\n}\nbody[data-runtime-mode=\"interactive\"] .scroll-window thead .column-filter-row.is-open th {\n position: sticky;\n top: 2.65rem;\n z-index: 1;\n background: var(--bg-raised);\n box-shadow: 0 1px 0 var(--line);\n}\n.entity-list { display: grid; gap: 1.5rem; }\n.entity-card {\n border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg);\n color: inherit;\n}\n.entity-card:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }\n.entity-card h3, .entity-card p { margin: 0; }\n.entity-card p + p, .entity-card strong + p, .entity-card h3 + p { margin-top: 0.35rem; }\n.panel > h2, .table-shell > h2, .graph-shell > h2, .print-card > h3 { margin-top: 0; margin-bottom: 0.75rem; }\n.toolbar h2 { margin: 0; }\n.rollup-list { display: grid; gap: 0.5rem; }\n.rollup-list div {\n display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--line); padding-bottom: 0.4rem;\n}\n.rollup-list dd { margin: 0; }\n.distribution-track {\n height: 0.7rem;\n min-width: 6rem;\n border-radius: 999px;\n background: var(--bg);\n border: 1px solid var(--line);\n overflow: hidden;\n}\n.distribution-fill {\n display: block;\n height: 100%;\n border-radius: inherit;\n background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));\n min-width: 0;\n}\n.graph-root svg {\n width: 100%; min-height: 620px; background: var(--bg); border-radius: 10px; border: 1px solid var(--line);\n}\n.graph-root line {\n stroke: color-mix(in srgb, var(--line) 80%, transparent); stroke-width: 1.2;\n opacity: 0.85; transition: opacity 180ms ease;\n}\n.graph-root .node { cursor: pointer; }\n.graph-root circle {\n fill: var(--accent); stroke: color-mix(in srgb, var(--fg) 35%, transparent); stroke-width: 1.25;\n transition: fill 180ms ease, stroke 180ms ease, r 180ms ease;\n}\n.graph-root .external circle { fill: var(--rust-muted); stroke: color-mix(in srgb, var(--rust) 50%, transparent); }\n.graph-root .ideaset circle { fill: color-mix(in srgb, #d18616 78%, white); stroke: #d18616; }\n.graph-root .subject circle {\n fill: var(--rust);\n stroke: var(--accent-strong);\n stroke-width: 2.75;\n animation: subject-pulse 2.4s ease-in-out infinite;\n}\n.graph-root text {\n fill: var(--fg); font-size: 11px; paint-order: stroke;\n stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round;\n pointer-events: none;\n}\n.graph-root .subject text {\n fill: var(--accent-strong); font-size: 12.5px; font-weight: 600;\n}\n.graph-root .node:hover circle { stroke: var(--accent-strong); stroke-width: 2; }\n.graph-root .graph-label {\n width: 160px;\n color: var(--fg);\n font: 11px/1.3 Inter, system-ui, sans-serif;\n text-align: center;\n word-break: break-word;\n overflow-wrap: anywhere;\n pointer-events: none;\n user-select: none;\n}\n.graph-root .graph-label strong { display: block; font-weight: 600; }\n.graph-root .graph-label .meta { display: block; color: var(--muted); font-size: 10px; margin-top: 0.15rem; }\n.graph-root .graph-label .attrs { display: block; color: var(--rust); font-size: 9.5px; margin-top: 0.1rem; }\n.graph-root .subject .graph-label strong { color: var(--accent-strong); }\n.graph-root svg { touch-action: none; cursor: grab; }\n.graph-root svg.is-panning { cursor: grabbing; }\n.graph-root .node { cursor: pointer; }\n@keyframes subject-pulse {\n 0%, 100% { filter: drop-shadow(0 0 0 transparent); }\n 50% { filter: drop-shadow(0 0 8px color-mix(in srgb, var(--rust) 55%, transparent)); }\n}\n.search-results { display: grid; gap: 0.7rem; margin-top: 1rem; }\n.search-result:hover { border-color: var(--accent-dim); }\n.hidden { display: none !important; }\n.breadcrumbs a { color: var(--muted); }\n.breadcrumbs a:hover { color: var(--accent-strong); }\n.section-link { font-size: 0.92rem; }\n.prose p { line-height: 1.55; }\n.idea-summary { white-space: normal; }\n.idea-ref {\n display: inline;\n padding: 0.05em 0.35em;\n margin: 0 0.05em;\n border-radius: 0.3em;\n border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));\n background: color-mix(in srgb, var(--accent) 14%, var(--bg-raised));\n color: var(--accent-strong);\n font-weight: 600;\n text-decoration: none;\n white-space: nowrap;\n}\na.idea-ref:hover {\n color: var(--fg);\n border-color: var(--accent);\n background: color-mix(in srgb, var(--accent) 22%, var(--bg-raised));\n}\n.idea-ref--file {\n border-color: color-mix(in srgb, var(--rust) 40%, var(--line));\n background: color-mix(in srgb, var(--rust) 16%, var(--bg-raised));\n color: #e8a07a;\n}\na.idea-ref--file:hover {\n color: var(--fg);\n border-color: var(--rust);\n}\n.idea-ref--unresolved {\n border-style: dashed;\n border-color: color-mix(in srgb, var(--warn) 45%, var(--line));\n background: color-mix(in srgb, var(--warn) 10%, var(--bg-raised));\n color: var(--warn);\n font-weight: 500;\n}\npre.code-like {\n white-space: pre-wrap; padding: 0.9rem; border-radius: 8px;\n background: var(--bg); border: 1px solid var(--line);\n}\n@media (max-width: 900px) {\n .hero, .topbar-inner { grid-template-columns: 1fr; }\n}\n@media print {\n :root { color-scheme: light; }\n body { background: white; color: black; }\n .topbar, .searchbar, .toolbar .actions, .graph-shell .toolbar, .column-filter-row, .table-filter-toggle, .table-filter-actions, .hide-on-print { display: none !important; }\n .sort-button { cursor: default; }\n .panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result, .entity-card {\n background: white; color: black; border-color: #cbd5e1; box-shadow: none;\n }\n .scroll-window {\n max-height: none !important;\n overflow: visible !important;\n border: none !important;\n background: transparent !important;\n }\n .scroll-window thead th {\n position: static !important;\n box-shadow: none !important;\n }\n a { color: black; text-decoration: underline; }\n .print-break-avoid { break-inside: avoid; }\n}\n";
2
- export declare const APP_JS = "\nconst searchIndexByRoot = new WeakMap();\n\nfunction exportRootPrefix(root) {\n const searchUrl = root.dataset.searchIndex || '';\n const dataIdx = searchUrl.lastIndexOf('data/');\n return dataIdx >= 0 ? searchUrl.slice(0, dataIdx) : '';\n}\n\nfunction resolveExportUrl(root, url) {\n const cleaned = String(url || '').replace(/^\\.\\//, '');\n return `${exportRootPrefix(root)}${cleaned}`;\n}\n\nasync function loadSearchIndex(root) {\n if (searchIndexByRoot.has(root)) return searchIndexByRoot.get(root);\n const embedded = typeof globalThis.__REQLAN_SEARCH_INDEX__ !== 'undefined'\n ? globalThis.__REQLAN_SEARCH_INDEX__\n : null;\n if (Array.isArray(embedded)) {\n const ready = Promise.resolve(embedded);\n searchIndexByRoot.set(root, ready);\n return ready;\n }\n const url = root.dataset.searchIndex;\n if (!url) return [];\n const loaded = fetch(url).then(response => {\n if (!response.ok) throw new Error('search index fetch failed');\n return response.json();\n }).catch(() => []);\n searchIndexByRoot.set(root, loaded);\n return loaded;\n}\n\nfunction lower(value) {\n return String(value || '').toLowerCase();\n}\n\nfunction scoreDocument(doc, query) {\n const haystack = [\n doc.title,\n doc.summary,\n ...(doc.tags || []),\n ...(doc.pathTokens || []),\n ...(doc.keywords || [])\n ].map(lower).join(' ');\n return haystack.includes(query) ? haystack.indexOf(query) : -1;\n}\n\nasync function wireGlobalSearch(root) {\n const input = root.querySelector('[data-global-search]');\n const results = root.querySelector('[data-search-results]');\n if (!input || !results) return;\n const docs = await loadSearchIndex(root);\n function render() {\n const query = lower(input.value.trim());\n if (!query) {\n results.innerHTML = '';\n results.classList.add('hidden');\n return;\n }\n const matches = docs\n .map(doc => ({ doc, score: scoreDocument(doc, query) }))\n .filter(item => item.score >= 0)\n .sort((a, b) => a.score - b.score || a.doc.title.localeCompare(b.doc.title))\n .slice(0, 18);\n results.classList.remove('hidden');\n results.innerHTML = matches.length === 0\n ? '<div class=\"search-result\"><strong>No matches</strong><p class=\"subtle\">Try idea names, tags, statuses, file paths, or cluster labels.</p></div>'\n : matches.map(({ doc }) => `\n <a class=\"search-result\" href=\"${resolveExportUrl(root, doc.url)}\">\n <div class=\"pill-row\">\n <span class=\"pill\">${doc.kind}</span>\n ${doc.status ? `<span class=\"pill\">${doc.status}</span>` : ''}\n </div>\n <strong>${doc.title}</strong>\n <p class=\"subtle\">${doc.summary || ''}</p>\n </a>\n `).join('');\n }\n input.addEventListener('input', render);\n}\n\nfunction cellSortValue(cell) {\n if (!cell) return '';\n const explicit = cell.getAttribute('data-sort-value');\n if (explicit != null) return explicit.trim();\n return String(cell.textContent || '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction compareSortValues(left, right) {\n const leftNumber = Number(String(left).replace(/,/g, ''));\n const rightNumber = Number(String(right).replace(/,/g, ''));\n const bothNumeric = left !== '' && right !== ''\n && Number.isFinite(leftNumber)\n && Number.isFinite(rightNumber);\n if (bothNumeric) {\n return leftNumber - rightNumber;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });\n}\n\nfunction isPlaceholderRow(row) {\n if (!row || row.cells.length === 0) return true;\n return [...row.cells].some(cell => Number(cell.colSpan || 1) > 1);\n}\n\nconst FILTER_ICON_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"></polygon></svg>';\n\nfunction ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState) {\n const shell = table.closest('.table-shell') || table.parentElement;\n const toolbar = shell ? shell.querySelector(':scope > .toolbar') : null;\n let actions = toolbar ? toolbar.querySelector('.actions') : null;\n if (toolbar && !actions) {\n actions = document.createElement('div');\n actions.className = 'actions';\n toolbar.appendChild(actions);\n }\n if (!actions) {\n actions = document.createElement('div');\n actions.className = 'actions table-filter-actions';\n table.parentElement?.insertBefore(actions, table);\n }\n\n const toggle = document.createElement('button');\n toggle.type = 'button';\n toggle.className = 'table-filter-toggle';\n toggle.setAttribute('aria-label', 'Toggle column filters');\n toggle.setAttribute('aria-expanded', 'false');\n toggle.setAttribute('title', 'Column filters');\n toggle.innerHTML = FILTER_ICON_SVG;\n toggle.addEventListener('click', () => {\n const open = !filterRow.classList.contains('is-open');\n filterRow.classList.toggle('is-open', open);\n toggle.classList.toggle('is-active', open);\n toggle.setAttribute('aria-expanded', open ? 'true' : 'false');\n syncToggleState();\n if (open) {\n filterInputs[0]?.focus();\n }\n });\n actions.appendChild(toggle);\n return toggle;\n}\n\nfunction wireTables(root) {\n const tables = root.querySelectorAll('table');\n const tableControllers = [];\n\n for (const table of tables) {\n const thead = table.tHead;\n const tbody = table.tBodies[0];\n if (!thead || !tbody || thead.rows.length === 0) continue;\n const headerRow = thead.rows[0];\n if (headerRow.dataset.enhanced === '1') continue;\n headerRow.dataset.enhanced = '1';\n\n const headers = [...headerRow.cells];\n if (headers.length === 0) continue;\n\n let sortIndex = -1;\n let sortDir = 'asc';\n const filterInputs = [];\n let filterToggle = null;\n const syncToggleState = () => {\n if (!filterToggle) return;\n const hasFilters = filterInputs.some(input => input.value.trim());\n filterToggle.classList.toggle('has-filters', hasFilters);\n };\n const controller = {\n table,\n tbody,\n filterInputs,\n applyFilters() {\n const sample = [...tbody.rows].find(row => row.hasAttribute('data-filter-row'));\n const filterId = sample?.getAttribute('data-filter-row');\n const globalInput = filterId\n ? root.querySelector(`[data-filter-input=\"${filterId}\"]`)\n : null;\n const globalQuery = lower(globalInput?.value.trim());\n const columnQueries = filterInputs.map(input => lower(input.value.trim()));\n for (const row of tbody.rows) {\n if (isPlaceholderRow(row)) continue;\n const globalText = lower(row.getAttribute('data-filter-text') || row.textContent);\n const matchesGlobal = !globalQuery || globalText.includes(globalQuery);\n const matchesColumns = columnQueries.every((query, index) => {\n if (!query) return true;\n return lower(cellSortValue(row.cells[index])).includes(query);\n });\n row.classList.toggle('hidden', !(matchesGlobal && matchesColumns));\n }\n syncToggleState();\n }\n };\n\n headers.forEach((th, index) => {\n const label = String(th.textContent || '').trim() || `Column ${index + 1}`;\n th.classList.add('sortable-th');\n th.replaceChildren();\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'sort-button';\n button.dataset.sortCol = String(index);\n button.setAttribute('aria-label', `Sort by ${label}`);\n const labelNode = document.createElement('span');\n labelNode.textContent = label;\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n indicator.setAttribute('aria-hidden', 'true');\n button.append(labelNode, indicator);\n button.addEventListener('click', () => {\n if (sortIndex === index) {\n sortDir = sortDir === 'asc' ? 'desc' : 'asc';\n } else {\n sortIndex = index;\n sortDir = 'asc';\n }\n const dataRows = [...tbody.rows].filter(row => !isPlaceholderRow(row));\n dataRows.sort((left, right) => {\n const cmp = compareSortValues(\n cellSortValue(left.cells[index]),\n cellSortValue(right.cells[index])\n );\n return sortDir === 'asc' ? cmp : -cmp;\n });\n for (const row of dataRows) {\n tbody.appendChild(row);\n }\n for (const header of headers) {\n const sortButton = header.querySelector('.sort-button');\n const mark = header.querySelector('.sort-indicator');\n if (!sortButton || !mark) continue;\n const active = Number(sortButton.dataset.sortCol) === sortIndex;\n sortButton.classList.toggle('sort-active', active);\n mark.textContent = active ? (sortDir === 'asc' ? '\u25B2' : '\u25BC') : '';\n }\n });\n th.appendChild(button);\n });\n\n const filterRow = document.createElement('tr');\n filterRow.className = 'column-filter-row';\n headers.forEach((th, index) => {\n const label = String(th.querySelector('.sort-button span')?.textContent || `Column ${index + 1}`);\n const cell = document.createElement('th');\n const input = document.createElement('input');\n input.type = 'search';\n input.className = 'column-filter';\n input.placeholder = 'Filter\u2026';\n input.dataset.colFilter = String(index);\n input.setAttribute('aria-label', `Filter ${label}`);\n input.addEventListener('input', () => controller.applyFilters());\n filterInputs.push(input);\n cell.appendChild(input);\n filterRow.appendChild(cell);\n });\n thead.appendChild(filterRow);\n filterToggle = ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState);\n tableControllers.push(controller);\n }\n\n const inputs = root.querySelectorAll('[data-filter-input]');\n for (const input of inputs) {\n input.addEventListener('input', () => {\n const targetId = input.dataset.filterInput;\n for (const controller of tableControllers) {\n const ownsRows = controller.tbody.querySelector(`[data-filter-row=\"${targetId}\"]`);\n if (ownsRows) {\n controller.applyFilters();\n }\n }\n });\n }\n}\n\nfunction hashAngle(seed) {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (hash * 31 + seed.charCodeAt(index)) | 0;\n }\n return ((hash >>> 0) % 6283) / 1000;\n}\n\n// Matches packages/extension/webviews/shared/graph/graph-physics.ts DEFAULT_PHYSICS_SETTINGS.\nconst EXPORT_PHYSICS_SETTINGS = {\n gravity: 0.002,\n repulsion: 20000,\n linkStrength: 0.015,\n linkDistance: 120,\n damping: 0.5,\n maxVelocity: 10,\n minSeparation: 24,\n restSpeed: 0.02,\n restTicks: 90\n};\n\nfunction wireGraph(root) {\n const graphRoots = root.querySelectorAll('[data-graph-json]');\n for (const element of graphRoots) {\n const scriptId = element.dataset.graphJson;\n const dataElement = root.querySelector(`#${scriptId}`);\n if (!dataElement) continue;\n let graph;\n try {\n graph = JSON.parse(dataElement.textContent || '{}');\n } catch {\n continue;\n }\n let controls = root.querySelector(`[data-graph-controls=\"${scriptId}\"]`);\n if (!controls) {\n controls = document.createElement('div');\n controls.className = 'graph-controls-bar';\n controls.dataset.graphControls = scriptId;\n controls.innerHTML = '<button type=\"button\" class=\"graph-action\" data-graph-toggle-physics aria-pressed=\"false\">Live physics</button><button type=\"button\" class=\"graph-action\" data-graph-fit>Fit</button><span class=\"graph-status\" data-graph-status-text></span>';\n element.parentElement?.insertBefore(controls, element);\n }\n const searchInput = controls.querySelector('[data-graph-search]');\n const pathInput = controls.querySelector('[data-graph-path]');\n const statusSelect = controls.querySelector('[data-graph-status]');\n const tagSelect = controls.querySelector('[data-graph-tag]');\n const toggleExternal = controls.querySelector('[data-graph-toggle-external]');\n const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');\n const fitButton = controls.querySelector('[data-graph-fit]');\n const resetButton = controls.querySelector('[data-graph-reset]');\n const statusText = controls.querySelector('[data-graph-status-text]');\n let hideExternal = false;\n let livePhysics = false;\n let animationFrame = 0;\n let calmTicks = 0;\n let simNodes = [];\n let simEdges = [];\n const positions = new Map();\n const velocities = new Map();\n let svg;\n let edgeEls = [];\n let nodeEls = [];\n const pinnedIds = new Set();\n let view = { x: 0, y: 0, w: 1200, h: 640 };\n let userViewport = false;\n let dragNodeId = null;\n let dragMoved = false;\n let panMode = false;\n let panOrigin = null;\n let suppressClickUntil = 0;\n\n function resolveGraphNodeUrl(pageUrl) {\n let cleaned = String(pageUrl || '').replace(/^\\.\\//, '');\n if (cleaned && !cleaned.includes('/') && cleaned.endsWith('.html')) {\n cleaned = `ideas/${cleaned}`;\n }\n return resolveExportUrl(root, cleaned);\n }\n\n function escapeXml(value) {\n return String(value || '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n\n function formatNodeMeta(node) {\n const bits = [];\n if (node.status) bits.push(String(node.status));\n if (Array.isArray(node.tags) && node.tags.length) bits.push(node.tags.join(', '));\n return bits.join(' \u00B7 ');\n }\n\n function formatNodeAttrs(node) {\n const keys = Array.isArray(node.attributeKeys)\n ? node.attributeKeys\n : Object.keys(node.attributes || {});\n return keys.length ? keys.map(key => '@' + key).join(' ') : '';\n }\n\n function seedMissingPositions(nodes, width, height, centerId) {\n const cx = width / 2;\n const cy = height / 2;\n const subject = nodes.find(node => node.isSubject || node.id === centerId);\n const radius = Math.min(width, height) * 0.34;\n nodes.forEach((node, index) => {\n if (positions.has(node.id)) return;\n if (subject && node.id === subject.id) {\n positions.set(node.id, { x: cx, y: cy });\n return;\n }\n const angle = (Math.PI * 2 * index) / Math.max(nodes.length, 1) - Math.PI / 2;\n const ring = radius * (0.72 + (index % 3) * 0.14);\n positions.set(node.id, {\n x: cx + Math.cos(angle) * ring,\n y: cy + Math.sin(angle) * ring\n });\n });\n }\n\n function pruneState(validIds) {\n for (const id of [...positions.keys()]) {\n if (!validIds.has(id)) positions.delete(id);\n }\n for (const id of [...velocities.keys()]) {\n if (!validIds.has(id)) velocities.delete(id);\n }\n }\n\n function physicsStep(nodes, edges) {\n const settings = EXPORT_PHYSICS_SETTINGS;\n const count = nodes.length;\n if (count === 0) return 0;\n const ids = new Array(count);\n const xs = new Float64Array(count);\n const ys = new Float64Array(count);\n const fxs = new Float64Array(count);\n const fys = new Float64Array(count);\n const indexById = new Map();\n for (let i = 0; i < count; i += 1) {\n const node = nodes[i];\n const pos = positions.get(node.id) || { x: 0, y: 0 };\n ids[i] = node.id;\n xs[i] = pos.x;\n ys[i] = pos.y;\n indexById.set(node.id, i);\n }\n let centroidX = 0;\n let centroidY = 0;\n for (let i = 0; i < count; i += 1) {\n centroidX += xs[i];\n centroidY += ys[i];\n }\n centroidX /= count;\n centroidY /= count;\n for (let i = 0; i < count; i += 1) {\n fxs[i] -= settings.gravity * (xs[i] - centroidX);\n fys[i] -= settings.gravity * (ys[i] - centroidY);\n }\n const minSeparationSq = settings.minSeparation * settings.minSeparation;\n for (let i = 0; i < count; i += 1) {\n for (let j = i + 1; j < count; j += 1) {\n let dx = xs[j] - xs[i];\n let dy = ys[j] - ys[i];\n let distSq = dx * dx + dy * dy;\n if (distSq < 1e-6) {\n const angle = hashAngle(ids[i] + ids[j]);\n dx = Math.cos(angle);\n dy = Math.sin(angle);\n distSq = 1;\n }\n const clampedSq = Math.max(distSq, minSeparationSq);\n const dist = Math.sqrt(distSq);\n const force = settings.repulsion / clampedSq;\n const fx = (dx / dist) * force;\n const fy = (dy / dist) * force;\n fxs[i] -= fx;\n fys[i] -= fy;\n fxs[j] += fx;\n fys[j] += fy;\n }\n }\n for (const edge of edges) {\n const sourceIndex = indexById.get(edge.sourceId);\n const targetIndex = indexById.get(edge.targetId);\n if (sourceIndex === undefined || targetIndex === undefined || sourceIndex === targetIndex) continue;\n const dx = xs[targetIndex] - xs[sourceIndex];\n const dy = ys[targetIndex] - ys[sourceIndex];\n const dist = Math.sqrt(dx * dx + dy * dy);\n if (dist < 1e-3) continue;\n const force = settings.linkStrength * (dist - settings.linkDistance);\n const fx = (dx / dist) * force;\n const fy = (dy / dist) * force;\n fxs[sourceIndex] += fx;\n fys[sourceIndex] += fy;\n fxs[targetIndex] -= fx;\n fys[targetIndex] -= fy;\n }\n let speedSum = 0;\n let movingCount = 0;\n for (let i = 0; i < count; i += 1) {\n const id = ids[i];\n if (pinnedIds.has(id)) {\n continue;\n }\n const velocity = velocities.get(id) || { vx: 0, vy: 0 };\n let vx = (velocity.vx + fxs[i]) * settings.damping;\n let vy = (velocity.vy + fys[i]) * settings.damping;\n const speed = Math.sqrt(vx * vx + vy * vy);\n if (speed > settings.maxVelocity) {\n const scale = settings.maxVelocity / speed;\n vx *= scale;\n vy *= scale;\n }\n velocity.vx = vx;\n velocity.vy = vy;\n velocities.set(id, velocity);\n positions.set(id, { x: xs[i] + vx, y: ys[i] + vy });\n speedSum += Math.min(speed, settings.maxVelocity);\n movingCount += 1;\n }\n return movingCount > 0 ? speedSum / movingCount : 0;\n }\n\n function contentBounds() {\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const pos of positions.values()) {\n minX = Math.min(minX, pos.x);\n minY = Math.min(minY, pos.y);\n maxX = Math.max(maxX, pos.x);\n maxY = Math.max(maxY, pos.y);\n }\n if (!Number.isFinite(minX)) {\n return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };\n }\n return { minX, minY, maxX, maxY };\n }\n\n function applyView() {\n if (!svg) return;\n svg.setAttribute('viewBox', `${view.x} ${view.y} ${view.w} ${view.h}`);\n }\n\n function fitView(force = false) {\n if (userViewport && !force) {\n applyView();\n return;\n }\n const bounds = contentBounds();\n const pad = 80;\n view = {\n x: bounds.minX - pad,\n y: bounds.minY - pad,\n w: Math.max(360, bounds.maxX - bounds.minX + pad * 2),\n h: Math.max(360, bounds.maxY - bounds.minY + pad * 2 + 70)\n };\n userViewport = false;\n applyView();\n }\n\n function paint() {\n if (!svg) return;\n for (const { node, circle, label } of nodeEls) {\n const pos = positions.get(node.id);\n if (!pos) continue;\n circle.setAttribute('cx', String(pos.x));\n circle.setAttribute('cy', String(pos.y));\n label.setAttribute('x', String(pos.x - 80));\n label.setAttribute('y', String(pos.y + 18));\n }\n for (const { edge, line } of edgeEls) {\n const source = positions.get(edge.sourceId);\n const target = positions.get(edge.targetId);\n if (!source || !target) continue;\n line.setAttribute('x1', String(source.x));\n line.setAttribute('y1', String(source.y));\n line.setAttribute('x2', String(target.x));\n line.setAttribute('y2', String(target.y));\n line.setAttribute('opacity', '0.85');\n }\n if (!userViewport) {\n fitView(false);\n } else {\n applyView();\n }\n }\n\n function stopLoop() {\n if (animationFrame) {\n cancelAnimationFrame(animationFrame);\n animationFrame = 0;\n }\n }\n\n function scheduleTick() {\n if (animationFrame) return;\n animationFrame = requestAnimationFrame(() => {\n animationFrame = 0;\n if (!livePhysics) return;\n const averageSpeed = physicsStep(simNodes, simEdges);\n paint();\n if (averageSpeed < EXPORT_PHYSICS_SETTINGS.restSpeed) {\n calmTicks += 1;\n if (calmTicks >= EXPORT_PHYSICS_SETTINGS.restTicks) {\n return;\n }\n } else {\n calmTicks = 0;\n }\n scheduleTick();\n });\n }\n\n function wake() {\n calmTicks = 0;\n if (livePhysics) scheduleTick();\n }\n\n function batchSettle(nodes, edges) {\n const iterations = Math.min(120, 48 + nodes.length);\n for (let i = 0; i < iterations; i += 1) {\n physicsStep(nodes, edges);\n }\n for (const velocity of velocities.values()) {\n velocity.vx = 0;\n velocity.vy = 0;\n }\n calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;\n }\n\n function mountGraph(filteredNodes, filteredEdges) {\n stopLoop();\n simNodes = filteredNodes;\n simEdges = filteredEdges;\n const validIds = new Set(filteredNodes.map(node => node.id));\n pruneState(validIds);\n const width = 1200;\n const height = Math.max(640, Math.min(1400, 220 + filteredNodes.length * 28));\n seedMissingPositions(filteredNodes, width, height, graph.centerId);\n svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n edgeEls = [];\n for (const edge of filteredEdges) {\n const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n svg.appendChild(line);\n edgeEls.push({ edge, line });\n }\n nodeEls = [];\n for (const node of filteredNodes) {\n const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');\n const classes = ['node'];\n if (node.isExternal) classes.push('external');\n if (node.kind === 'ideaset') classes.push('ideaset');\n if (node.isSubject || node.id === graph.centerId) classes.push('subject');\n group.setAttribute('class', classes.join(' '));\n group.dataset.nodeId = node.id;\n const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('r', node.isSubject || node.id === graph.centerId ? '18' : '13');\n const label = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');\n label.setAttribute('width', '160');\n label.setAttribute('height', '96');\n const meta = formatNodeMeta(node);\n const attrs = formatNodeAttrs(node);\n label.innerHTML = `<div xmlns=\"http://www.w3.org/1999/xhtml\" class=\"graph-label\"><strong>${escapeXml(node.name)}</strong>${meta ? `<span class=\"meta\">${escapeXml(meta)}</span>` : ''}${attrs ? `<span class=\"attrs\">${escapeXml(attrs)}</span>` : ''}</div>`;\n const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');\n title.textContent = [node.name, meta, attrs].filter(Boolean).join('\\n');\n group.appendChild(title);\n group.appendChild(circle);\n group.appendChild(label);\n svg.appendChild(group);\n nodeEls.push({ node, circle, label, group });\n }\n element.innerHTML = '';\n element.appendChild(svg);\n wireViewport(svg);\n if (statusText) {\n statusText.textContent = `${filteredNodes.length} nodes, ${filteredEdges.length} edges`;\n }\n userViewport = false;\n if (!livePhysics) {\n batchSettle(filteredNodes, filteredEdges);\n paint();\n } else {\n paint();\n wake();\n }\n }\n\n function clientToGraph(event) {\n const rect = svg.getBoundingClientRect();\n const x = view.x + ((event.clientX - rect.left) / rect.width) * view.w;\n const y = view.y + ((event.clientY - rect.top) / rect.height) * view.h;\n return { x, y };\n }\n\n function wireViewport(target) {\n target.addEventListener('wheel', (event) => {\n event.preventDefault();\n const factor = event.deltaY < 0 ? 0.9 : 1.1;\n const before = clientToGraph(event);\n view.w = Math.min(8000, Math.max(180, view.w * factor));\n view.h = Math.min(8000, Math.max(180, view.h * factor));\n const after = clientToGraph(event);\n view.x += before.x - after.x;\n view.y += before.y - after.y;\n userViewport = true;\n applyView();\n }, { passive: false });\n\n target.addEventListener('pointerdown', (event) => {\n const nodeGroup = event.target.closest?.('.node');\n if (nodeGroup) {\n const nodeId = nodeGroup.dataset.nodeId;\n dragNodeId = nodeId;\n dragMoved = false;\n pinnedIds.add(nodeId);\n velocities.set(nodeId, { vx: 0, vy: 0 });\n target.setPointerCapture(event.pointerId);\n event.preventDefault();\n return;\n }\n panMode = true;\n panOrigin = { x: event.clientX, y: event.clientY, viewX: view.x, viewY: view.y };\n target.classList.add('is-panning');\n target.setPointerCapture(event.pointerId);\n event.preventDefault();\n });\n\n target.addEventListener('pointermove', (event) => {\n if (dragNodeId) {\n const point = clientToGraph(event);\n positions.set(dragNodeId, { x: point.x, y: point.y });\n dragMoved = true;\n wake();\n paint();\n return;\n }\n if (panMode && panOrigin) {\n const rect = target.getBoundingClientRect();\n const dx = ((event.clientX - panOrigin.x) / rect.width) * view.w;\n const dy = ((event.clientY - panOrigin.y) / rect.height) * view.h;\n view.x = panOrigin.viewX - dx;\n view.y = panOrigin.viewY - dy;\n userViewport = true;\n applyView();\n }\n });\n\n target.addEventListener('pointerup', (event) => {\n if (dragNodeId) {\n const node = simNodes.find(item => item.id === dragNodeId);\n const moved = dragMoved;\n const nodeId = dragNodeId;\n dragNodeId = null;\n pinnedIds.delete(nodeId);\n wake();\n if (!moved && node?.pageUrl && Date.now() > suppressClickUntil) {\n window.location.href = resolveGraphNodeUrl(node.pageUrl);\n } else if (moved) {\n suppressClickUntil = Date.now() + 250;\n }\n return;\n }\n if (panMode) {\n panMode = false;\n panOrigin = null;\n target.classList.remove('is-panning');\n }\n });\n\n target.addEventListener('pointercancel', () => {\n if (dragNodeId) {\n pinnedIds.delete(dragNodeId);\n dragNodeId = null;\n wake();\n }\n panMode = false;\n panOrigin = null;\n target.classList.remove('is-panning');\n });\n }\n\n function matches(node) {\n const query = lower(searchInput?.value?.trim());\n const pathQuery = lower(pathInput?.value?.trim());\n const statusQuery = lower(statusSelect?.value?.trim());\n const tagQuery = lower(tagSelect?.value?.trim());\n if (hideExternal && node.isExternal) return false;\n if (hideIdeasets && node.kind === 'ideaset') return false;\n const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');\n if (query && !haystack.includes(query)) return false;\n if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;\n if (statusQuery && lower(node.status || '') !== statusQuery) return false;\n if (tagQuery && !(node.tags || []).some(tag => lower(tag) === tagQuery)) return false;\n return true;\n }\n\n function refresh() {\n const visibleNodes = (graph.nodes || []).filter(matches);\n const visibleIds = new Set(visibleNodes.map(node => node.id));\n const visibleEdges = (graph.edges || []).filter(edge =>\n visibleIds.has(edge.sourceId) && visibleIds.has(edge.targetId)\n );\n mountGraph(visibleNodes, visibleEdges);\n }\n\n for (const control of [searchInput, pathInput, statusSelect, tagSelect]) {\n control?.addEventListener('input', refresh);\n control?.addEventListener('change', refresh);\n }\n toggleExternal?.addEventListener('click', () => {\n hideExternal = !hideExternal;\n toggleExternal.classList.toggle('is-active', hideExternal);\n toggleExternal.textContent = hideExternal ? 'Show external' : 'Hide external';\n refresh();\n });\n toggleIdeasets?.addEventListener('click', () => {\n hideIdeasets = !hideIdeasets;\n toggleIdeasets.classList.toggle('is-active', hideIdeasets);\n toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';\n refresh();\n });\n togglePhysics?.addEventListener('click', () => {\n livePhysics = !livePhysics;\n togglePhysics.classList.toggle('is-active', livePhysics);\n togglePhysics.setAttribute('aria-pressed', livePhysics ? 'true' : 'false');\n if (livePhysics) {\n wake();\n } else {\n stopLoop();\n }\n });\n fitButton?.addEventListener('click', () => {\n fitView(true);\n });\n resetButton?.addEventListener('click', () => {\n if (searchInput) searchInput.value = '';\n if (pathInput) pathInput.value = '';\n if (statusSelect) statusSelect.value = '';\n if (tagSelect) tagSelect.value = '';\n hideExternal = false;\n hideIdeasets = false;\n toggleExternal?.classList.remove('is-active');\n toggleIdeasets?.classList.remove('is-active');\n if (toggleExternal) toggleExternal.textContent = 'Hide external';\n if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';\n positions.clear();\n velocities.clear();\n pinnedIds.clear();\n userViewport = false;\n refresh();\n });\n refresh();\n }\n}\n\nfunction boot() {\n const root = document.body;\n wireTables(root);\n wireGraph(root);\n void wireGlobalSearch(root);\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', boot);\n} else {\n boot();\n}\n";
2
+ export declare const APP_JS = "\nconst searchIndexByRoot = new WeakMap();\n\nfunction exportRootPrefix(root) {\n const searchUrl = root.dataset.searchIndex || '';\n const dataIdx = searchUrl.lastIndexOf('data/');\n return dataIdx >= 0 ? searchUrl.slice(0, dataIdx) : '';\n}\n\nfunction resolveExportUrl(root, url) {\n const cleaned = String(url || '').replace(/^\\.\\//, '');\n return `${exportRootPrefix(root)}${cleaned}`;\n}\n\nasync function loadSearchIndex(root) {\n if (searchIndexByRoot.has(root)) return searchIndexByRoot.get(root);\n const embedded = typeof globalThis.__REQLAN_SEARCH_INDEX__ !== 'undefined'\n ? globalThis.__REQLAN_SEARCH_INDEX__\n : null;\n if (Array.isArray(embedded)) {\n const ready = Promise.resolve(embedded);\n searchIndexByRoot.set(root, ready);\n return ready;\n }\n const url = root.dataset.searchIndex;\n if (!url) return [];\n const loaded = fetch(url).then(response => {\n if (!response.ok) throw new Error('search index fetch failed');\n return response.json();\n }).catch(() => []);\n searchIndexByRoot.set(root, loaded);\n return loaded;\n}\n\nfunction lower(value) {\n return String(value || '').toLowerCase();\n}\n\nfunction scoreDocument(doc, query) {\n const haystack = [\n doc.title,\n doc.summary,\n ...(doc.tags || []),\n ...(doc.pathTokens || []),\n ...(doc.keywords || [])\n ].map(lower).join(' ');\n return haystack.includes(query) ? haystack.indexOf(query) : -1;\n}\n\nasync function wireGlobalSearch(root) {\n const input = root.querySelector('[data-global-search]');\n const results = root.querySelector('[data-search-results]');\n if (!input || !results) return;\n const docs = await loadSearchIndex(root);\n function render() {\n const query = lower(input.value.trim());\n if (!query) {\n results.innerHTML = '';\n results.classList.add('hidden');\n return;\n }\n const matches = docs\n .map(doc => ({ doc, score: scoreDocument(doc, query) }))\n .filter(item => item.score >= 0)\n .sort((a, b) => a.score - b.score || a.doc.title.localeCompare(b.doc.title))\n .slice(0, 18);\n results.classList.remove('hidden');\n results.innerHTML = matches.length === 0\n ? '<div class=\"search-result\"><strong>No matches</strong><p class=\"subtle\">Try idea names, tags, statuses, file paths, or cluster labels.</p></div>'\n : matches.map(({ doc }) => `\n <a class=\"search-result\" href=\"${resolveExportUrl(root, doc.url)}\">\n <div class=\"pill-row\">\n <span class=\"pill\">${doc.kind}</span>\n ${doc.status ? `<span class=\"pill\">${doc.status}</span>` : ''}\n </div>\n <strong>${doc.title}</strong>\n <p class=\"subtle\">${doc.summary || ''}</p>\n </a>\n `).join('');\n }\n input.addEventListener('input', render);\n}\n\nfunction cellSortValue(cell) {\n if (!cell) return '';\n const explicit = cell.getAttribute('data-sort-value');\n if (explicit != null) return explicit.trim();\n return String(cell.textContent || '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction compareSortValues(left, right) {\n const leftNumber = Number(String(left).replace(/,/g, ''));\n const rightNumber = Number(String(right).replace(/,/g, ''));\n const bothNumeric = left !== '' && right !== ''\n && Number.isFinite(leftNumber)\n && Number.isFinite(rightNumber);\n if (bothNumeric) {\n return leftNumber - rightNumber;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });\n}\n\nfunction isPlaceholderRow(row) {\n if (!row || row.cells.length === 0) return true;\n return [...row.cells].some(cell => Number(cell.colSpan || 1) > 1);\n}\n\nconst FILTER_ICON_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"></polygon></svg>';\n\nfunction ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState) {\n const shell = table.closest('.table-shell') || table.parentElement;\n const toolbar = shell ? shell.querySelector(':scope > .toolbar') : null;\n let actions = toolbar ? toolbar.querySelector('.actions') : null;\n if (toolbar && !actions) {\n actions = document.createElement('div');\n actions.className = 'actions';\n toolbar.appendChild(actions);\n }\n if (!actions) {\n actions = document.createElement('div');\n actions.className = 'actions table-filter-actions';\n table.parentElement?.insertBefore(actions, table);\n }\n\n const toggle = document.createElement('button');\n toggle.type = 'button';\n toggle.className = 'table-filter-toggle';\n toggle.setAttribute('aria-label', 'Toggle column filters');\n toggle.setAttribute('aria-expanded', 'false');\n toggle.setAttribute('title', 'Column filters');\n toggle.innerHTML = FILTER_ICON_SVG;\n toggle.addEventListener('click', () => {\n const open = !filterRow.classList.contains('is-open');\n filterRow.classList.toggle('is-open', open);\n toggle.classList.toggle('is-active', open);\n toggle.setAttribute('aria-expanded', open ? 'true' : 'false');\n syncToggleState();\n if (open) {\n filterInputs[0]?.focus();\n }\n });\n actions.appendChild(toggle);\n return toggle;\n}\n\nfunction wireTables(root) {\n const tables = root.querySelectorAll('table');\n const tableControllers = [];\n\n for (const table of tables) {\n const thead = table.tHead;\n const tbody = table.tBodies[0];\n if (!thead || !tbody || thead.rows.length === 0) continue;\n const headerRow = thead.rows[0];\n if (headerRow.dataset.enhanced === '1') continue;\n headerRow.dataset.enhanced = '1';\n\n const headers = [...headerRow.cells];\n if (headers.length === 0) continue;\n\n let sortIndex = -1;\n let sortDir = 'asc';\n const filterInputs = [];\n let filterToggle = null;\n const syncToggleState = () => {\n if (!filterToggle) return;\n const hasFilters = filterInputs.some(input => input.value.trim());\n filterToggle.classList.toggle('has-filters', hasFilters);\n };\n const controller = {\n table,\n tbody,\n filterInputs,\n applyFilters() {\n const sample = [...tbody.rows].find(row => row.hasAttribute('data-filter-row'));\n const filterId = sample?.getAttribute('data-filter-row');\n const globalInput = filterId\n ? root.querySelector(`[data-filter-input=\"${filterId}\"]`)\n : null;\n const globalQuery = lower(globalInput?.value.trim());\n const columnQueries = filterInputs.map(input => lower(input.value.trim()));\n for (const row of tbody.rows) {\n if (isPlaceholderRow(row)) continue;\n const globalText = lower(row.getAttribute('data-filter-text') || row.textContent);\n const matchesGlobal = !globalQuery || globalText.includes(globalQuery);\n const matchesColumns = columnQueries.every((query, index) => {\n if (!query) return true;\n return lower(cellSortValue(row.cells[index])).includes(query);\n });\n row.classList.toggle('hidden', !(matchesGlobal && matchesColumns));\n }\n syncToggleState();\n }\n };\n\n headers.forEach((th, index) => {\n const label = String(th.textContent || '').trim() || `Column ${index + 1}`;\n th.classList.add('sortable-th');\n th.replaceChildren();\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'sort-button';\n button.dataset.sortCol = String(index);\n button.setAttribute('aria-label', `Sort by ${label}`);\n const labelNode = document.createElement('span');\n labelNode.textContent = label;\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n indicator.setAttribute('aria-hidden', 'true');\n button.append(labelNode, indicator);\n button.addEventListener('click', () => {\n if (sortIndex === index) {\n sortDir = sortDir === 'asc' ? 'desc' : 'asc';\n } else {\n sortIndex = index;\n sortDir = 'asc';\n }\n const dataRows = [...tbody.rows].filter(row => !isPlaceholderRow(row));\n dataRows.sort((left, right) => {\n const cmp = compareSortValues(\n cellSortValue(left.cells[index]),\n cellSortValue(right.cells[index])\n );\n return sortDir === 'asc' ? cmp : -cmp;\n });\n for (const row of dataRows) {\n tbody.appendChild(row);\n }\n for (const header of headers) {\n const sortButton = header.querySelector('.sort-button');\n const mark = header.querySelector('.sort-indicator');\n if (!sortButton || !mark) continue;\n const active = Number(sortButton.dataset.sortCol) === sortIndex;\n sortButton.classList.toggle('sort-active', active);\n mark.textContent = active ? (sortDir === 'asc' ? '\u25B2' : '\u25BC') : '';\n }\n });\n th.appendChild(button);\n });\n\n const filterRow = document.createElement('tr');\n filterRow.className = 'column-filter-row';\n headers.forEach((th, index) => {\n const label = String(th.querySelector('.sort-button span')?.textContent || `Column ${index + 1}`);\n const cell = document.createElement('th');\n const input = document.createElement('input');\n input.type = 'search';\n input.className = 'column-filter';\n input.placeholder = 'Filter\u2026';\n input.dataset.colFilter = String(index);\n input.setAttribute('aria-label', `Filter ${label}`);\n input.addEventListener('input', () => controller.applyFilters());\n filterInputs.push(input);\n cell.appendChild(input);\n filterRow.appendChild(cell);\n });\n thead.appendChild(filterRow);\n filterToggle = ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState);\n tableControllers.push(controller);\n }\n\n const inputs = root.querySelectorAll('[data-filter-input]');\n for (const input of inputs) {\n input.addEventListener('input', () => {\n const targetId = input.dataset.filterInput;\n for (const controller of tableControllers) {\n const ownsRows = controller.tbody.querySelector(`[data-filter-row=\"${targetId}\"]`);\n if (ownsRows) {\n controller.applyFilters();\n }\n }\n });\n }\n}\n\nfunction hashAngle(seed) {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (hash * 31 + seed.charCodeAt(index)) | 0;\n }\n return ((hash >>> 0) % 6283) / 1000;\n}\n\n// Matches packages/extension/webviews/shared/graph/graph-physics.ts DEFAULT_PHYSICS_SETTINGS.\nconst EXPORT_PHYSICS_SETTINGS = {\n gravity: 0.002,\n repulsion: 20000,\n linkStrength: 0.015,\n linkDistance: 120,\n damping: 0.5,\n maxVelocity: 10,\n minSeparation: 24,\n restSpeed: 0.02,\n restTicks: 90\n};\n\nfunction wireGraph(root) {\n const graphRoots = root.querySelectorAll('[data-graph-json]');\n for (const element of graphRoots) {\n const scriptId = element.dataset.graphJson;\n const dataElement = root.querySelector(`#${scriptId}`);\n if (!dataElement) continue;\n let graph;\n try {\n graph = JSON.parse(dataElement.textContent || '{}');\n } catch {\n continue;\n }\n let controls = root.querySelector(`[data-graph-controls=\"${scriptId}\"]`);\n if (!controls) {\n controls = document.createElement('div');\n controls.className = 'graph-controls-bar';\n controls.dataset.graphControls = scriptId;\n controls.innerHTML = '<button type=\"button\" class=\"graph-action\" data-graph-toggle-physics aria-pressed=\"false\">Live physics</button><button type=\"button\" class=\"graph-action\" data-graph-fit>Fit</button><span class=\"graph-status\" data-graph-status-text></span>';\n element.parentElement?.insertBefore(controls, element);\n }\n const searchInput = controls.querySelector('[data-graph-search]');\n const pathInput = controls.querySelector('[data-graph-path]');\n const statusSelect = controls.querySelector('[data-graph-status]');\n const tagSelect = controls.querySelector('[data-graph-tag]');\n const toggleExternal = controls.querySelector('[data-graph-toggle-external]');\n const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');\n const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');\n const fitButton = controls.querySelector('[data-graph-fit]');\n const resetButton = controls.querySelector('[data-graph-reset]');\n const statusText = controls.querySelector('[data-graph-status-text]');\n let hideExternal = false;\n let hideIdeasets = false;\n let livePhysics = false;\n let animationFrame = 0;\n let calmTicks = 0;\n let simNodes = [];\n let simEdges = [];\n const positions = new Map();\n const velocities = new Map();\n let svg;\n let edgeEls = [];\n let nodeEls = [];\n const pinnedIds = new Set();\n let view = { x: 0, y: 0, w: 1200, h: 640 };\n let userViewport = false;\n let dragNodeId = null;\n let dragMoved = false;\n let panMode = false;\n let panOrigin = null;\n let suppressClickUntil = 0;\n\n function resolveGraphNodeUrl(pageUrl) {\n let cleaned = String(pageUrl || '').replace(/^\\.\\//, '');\n if (cleaned && !cleaned.includes('/') && cleaned.endsWith('.html')) {\n cleaned = `ideas/${cleaned}`;\n }\n return resolveExportUrl(root, cleaned);\n }\n\n function escapeXml(value) {\n return String(value || '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n\n function formatNodeMeta(node) {\n const bits = [];\n if (node.status) bits.push(String(node.status));\n if (Array.isArray(node.tags) && node.tags.length) bits.push(node.tags.join(', '));\n return bits.join(' \u00B7 ');\n }\n\n function formatNodeAttrs(node) {\n const keys = Array.isArray(node.attributeKeys)\n ? node.attributeKeys\n : Object.keys(node.attributes || {});\n return keys.length ? keys.map(key => '@' + key).join(' ') : '';\n }\n\n function seedMissingPositions(nodes, width, height, centerId) {\n const cx = width / 2;\n const cy = height / 2;\n const subject = nodes.find(node => node.isSubject || node.id === centerId);\n const radius = Math.min(width, height) * 0.34;\n nodes.forEach((node, index) => {\n if (positions.has(node.id)) return;\n if (subject && node.id === subject.id) {\n positions.set(node.id, { x: cx, y: cy });\n return;\n }\n const angle = (Math.PI * 2 * index) / Math.max(nodes.length, 1) - Math.PI / 2;\n const ring = radius * (0.72 + (index % 3) * 0.14);\n positions.set(node.id, {\n x: cx + Math.cos(angle) * ring,\n y: cy + Math.sin(angle) * ring\n });\n });\n }\n\n function pruneState(validIds) {\n for (const id of [...positions.keys()]) {\n if (!validIds.has(id)) positions.delete(id);\n }\n for (const id of [...velocities.keys()]) {\n if (!validIds.has(id)) velocities.delete(id);\n }\n }\n\n function physicsStep(nodes, edges) {\n const settings = EXPORT_PHYSICS_SETTINGS;\n const count = nodes.length;\n if (count === 0) return 0;\n const ids = new Array(count);\n const xs = new Float64Array(count);\n const ys = new Float64Array(count);\n const fxs = new Float64Array(count);\n const fys = new Float64Array(count);\n const indexById = new Map();\n for (let i = 0; i < count; i += 1) {\n const node = nodes[i];\n const pos = positions.get(node.id) || { x: 0, y: 0 };\n ids[i] = node.id;\n xs[i] = pos.x;\n ys[i] = pos.y;\n indexById.set(node.id, i);\n }\n let centroidX = 0;\n let centroidY = 0;\n for (let i = 0; i < count; i += 1) {\n centroidX += xs[i];\n centroidY += ys[i];\n }\n centroidX /= count;\n centroidY /= count;\n for (let i = 0; i < count; i += 1) {\n fxs[i] -= settings.gravity * (xs[i] - centroidX);\n fys[i] -= settings.gravity * (ys[i] - centroidY);\n }\n const minSeparationSq = settings.minSeparation * settings.minSeparation;\n for (let i = 0; i < count; i += 1) {\n for (let j = i + 1; j < count; j += 1) {\n let dx = xs[j] - xs[i];\n let dy = ys[j] - ys[i];\n let distSq = dx * dx + dy * dy;\n if (distSq < 1e-6) {\n const angle = hashAngle(ids[i] + ids[j]);\n dx = Math.cos(angle);\n dy = Math.sin(angle);\n distSq = 1;\n }\n const clampedSq = Math.max(distSq, minSeparationSq);\n const dist = Math.sqrt(distSq);\n const force = settings.repulsion / clampedSq;\n const fx = (dx / dist) * force;\n const fy = (dy / dist) * force;\n fxs[i] -= fx;\n fys[i] -= fy;\n fxs[j] += fx;\n fys[j] += fy;\n }\n }\n for (const edge of edges) {\n const sourceIndex = indexById.get(edge.sourceId);\n const targetIndex = indexById.get(edge.targetId);\n if (sourceIndex === undefined || targetIndex === undefined || sourceIndex === targetIndex) continue;\n const dx = xs[targetIndex] - xs[sourceIndex];\n const dy = ys[targetIndex] - ys[sourceIndex];\n const dist = Math.sqrt(dx * dx + dy * dy);\n if (dist < 1e-3) continue;\n const force = settings.linkStrength * (dist - settings.linkDistance);\n const fx = (dx / dist) * force;\n const fy = (dy / dist) * force;\n fxs[sourceIndex] += fx;\n fys[sourceIndex] += fy;\n fxs[targetIndex] -= fx;\n fys[targetIndex] -= fy;\n }\n let speedSum = 0;\n let movingCount = 0;\n for (let i = 0; i < count; i += 1) {\n const id = ids[i];\n if (pinnedIds.has(id)) {\n continue;\n }\n const velocity = velocities.get(id) || { vx: 0, vy: 0 };\n let vx = (velocity.vx + fxs[i]) * settings.damping;\n let vy = (velocity.vy + fys[i]) * settings.damping;\n const speed = Math.sqrt(vx * vx + vy * vy);\n if (speed > settings.maxVelocity) {\n const scale = settings.maxVelocity / speed;\n vx *= scale;\n vy *= scale;\n }\n velocity.vx = vx;\n velocity.vy = vy;\n velocities.set(id, velocity);\n positions.set(id, { x: xs[i] + vx, y: ys[i] + vy });\n speedSum += Math.min(speed, settings.maxVelocity);\n movingCount += 1;\n }\n return movingCount > 0 ? speedSum / movingCount : 0;\n }\n\n function contentBounds() {\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const pos of positions.values()) {\n minX = Math.min(minX, pos.x);\n minY = Math.min(minY, pos.y);\n maxX = Math.max(maxX, pos.x);\n maxY = Math.max(maxY, pos.y);\n }\n if (!Number.isFinite(minX)) {\n return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };\n }\n return { minX, minY, maxX, maxY };\n }\n\n function applyView() {\n if (!svg) return;\n svg.setAttribute('viewBox', `${view.x} ${view.y} ${view.w} ${view.h}`);\n }\n\n function fitView(force = false) {\n if (userViewport && !force) {\n applyView();\n return;\n }\n const bounds = contentBounds();\n const pad = 80;\n view = {\n x: bounds.minX - pad,\n y: bounds.minY - pad,\n w: Math.max(360, bounds.maxX - bounds.minX + pad * 2),\n h: Math.max(360, bounds.maxY - bounds.minY + pad * 2 + 70)\n };\n userViewport = false;\n applyView();\n }\n\n function paint() {\n if (!svg) return;\n for (const { node, circle, label } of nodeEls) {\n const pos = positions.get(node.id);\n if (!pos) continue;\n circle.setAttribute('cx', String(pos.x));\n circle.setAttribute('cy', String(pos.y));\n label.setAttribute('x', String(pos.x - 80));\n label.setAttribute('y', String(pos.y + 18));\n }\n for (const { edge, line } of edgeEls) {\n const source = positions.get(edge.sourceId);\n const target = positions.get(edge.targetId);\n if (!source || !target) continue;\n line.setAttribute('x1', String(source.x));\n line.setAttribute('y1', String(source.y));\n line.setAttribute('x2', String(target.x));\n line.setAttribute('y2', String(target.y));\n line.setAttribute('opacity', '0.85');\n }\n if (!userViewport) {\n fitView(false);\n } else {\n applyView();\n }\n }\n\n function stopLoop() {\n if (animationFrame) {\n cancelAnimationFrame(animationFrame);\n animationFrame = 0;\n }\n }\n\n function scheduleTick() {\n if (animationFrame) return;\n animationFrame = requestAnimationFrame(() => {\n animationFrame = 0;\n if (!livePhysics) return;\n const averageSpeed = physicsStep(simNodes, simEdges);\n paint();\n if (averageSpeed < EXPORT_PHYSICS_SETTINGS.restSpeed) {\n calmTicks += 1;\n if (calmTicks >= EXPORT_PHYSICS_SETTINGS.restTicks) {\n return;\n }\n } else {\n calmTicks = 0;\n }\n scheduleTick();\n });\n }\n\n function wake() {\n calmTicks = 0;\n if (livePhysics) scheduleTick();\n }\n\n function batchSettle(nodes, edges) {\n const iterations = Math.min(120, 48 + nodes.length);\n for (let i = 0; i < iterations; i += 1) {\n physicsStep(nodes, edges);\n }\n for (const velocity of velocities.values()) {\n velocity.vx = 0;\n velocity.vy = 0;\n }\n calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;\n }\n\n function mountGraph(filteredNodes, filteredEdges) {\n stopLoop();\n simNodes = filteredNodes;\n simEdges = filteredEdges;\n const validIds = new Set(filteredNodes.map(node => node.id));\n pruneState(validIds);\n const width = 1200;\n const height = Math.max(640, Math.min(1400, 220 + filteredNodes.length * 28));\n seedMissingPositions(filteredNodes, width, height, graph.centerId);\n svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n edgeEls = [];\n for (const edge of filteredEdges) {\n const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n svg.appendChild(line);\n edgeEls.push({ edge, line });\n }\n nodeEls = [];\n for (const node of filteredNodes) {\n const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');\n const classes = ['node'];\n if (node.isExternal) classes.push('external');\n if (node.kind === 'ideaset') classes.push('ideaset');\n if (node.isSubject || node.id === graph.centerId) classes.push('subject');\n group.setAttribute('class', classes.join(' '));\n group.dataset.nodeId = node.id;\n const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n circle.setAttribute('r', node.isSubject || node.id === graph.centerId ? '18' : '13');\n const label = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');\n label.setAttribute('width', '160');\n label.setAttribute('height', '96');\n const meta = formatNodeMeta(node);\n const attrs = formatNodeAttrs(node);\n label.innerHTML = `<div xmlns=\"http://www.w3.org/1999/xhtml\" class=\"graph-label\"><strong>${escapeXml(node.name)}</strong>${meta ? `<span class=\"meta\">${escapeXml(meta)}</span>` : ''}${attrs ? `<span class=\"attrs\">${escapeXml(attrs)}</span>` : ''}</div>`;\n const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');\n title.textContent = [node.name, meta, attrs].filter(Boolean).join('\\n');\n group.appendChild(title);\n group.appendChild(circle);\n group.appendChild(label);\n svg.appendChild(group);\n nodeEls.push({ node, circle, label, group });\n }\n element.innerHTML = '';\n element.appendChild(svg);\n wireViewport(svg);\n if (statusText) {\n statusText.textContent = `${filteredNodes.length} nodes, ${filteredEdges.length} edges`;\n }\n userViewport = false;\n if (!livePhysics) {\n batchSettle(filteredNodes, filteredEdges);\n paint();\n } else {\n paint();\n wake();\n }\n }\n\n function clientToGraph(event) {\n const rect = svg.getBoundingClientRect();\n const x = view.x + ((event.clientX - rect.left) / rect.width) * view.w;\n const y = view.y + ((event.clientY - rect.top) / rect.height) * view.h;\n return { x, y };\n }\n\n function wireViewport(target) {\n target.addEventListener('wheel', (event) => {\n event.preventDefault();\n const factor = event.deltaY < 0 ? 0.9 : 1.1;\n const before = clientToGraph(event);\n view.w = Math.min(8000, Math.max(180, view.w * factor));\n view.h = Math.min(8000, Math.max(180, view.h * factor));\n const after = clientToGraph(event);\n view.x += before.x - after.x;\n view.y += before.y - after.y;\n userViewport = true;\n applyView();\n }, { passive: false });\n\n target.addEventListener('pointerdown', (event) => {\n const nodeGroup = event.target.closest?.('.node');\n if (nodeGroup) {\n const nodeId = nodeGroup.dataset.nodeId;\n dragNodeId = nodeId;\n dragMoved = false;\n pinnedIds.add(nodeId);\n velocities.set(nodeId, { vx: 0, vy: 0 });\n target.setPointerCapture(event.pointerId);\n event.preventDefault();\n return;\n }\n panMode = true;\n panOrigin = { x: event.clientX, y: event.clientY, viewX: view.x, viewY: view.y };\n target.classList.add('is-panning');\n target.setPointerCapture(event.pointerId);\n event.preventDefault();\n });\n\n target.addEventListener('pointermove', (event) => {\n if (dragNodeId) {\n const point = clientToGraph(event);\n positions.set(dragNodeId, { x: point.x, y: point.y });\n dragMoved = true;\n wake();\n paint();\n return;\n }\n if (panMode && panOrigin) {\n const rect = target.getBoundingClientRect();\n const dx = ((event.clientX - panOrigin.x) / rect.width) * view.w;\n const dy = ((event.clientY - panOrigin.y) / rect.height) * view.h;\n view.x = panOrigin.viewX - dx;\n view.y = panOrigin.viewY - dy;\n userViewport = true;\n applyView();\n }\n });\n\n target.addEventListener('pointerup', (event) => {\n if (dragNodeId) {\n const node = simNodes.find(item => item.id === dragNodeId);\n const moved = dragMoved;\n const nodeId = dragNodeId;\n dragNodeId = null;\n pinnedIds.delete(nodeId);\n wake();\n if (!moved && node?.pageUrl && Date.now() > suppressClickUntil) {\n window.location.href = resolveGraphNodeUrl(node.pageUrl);\n } else if (moved) {\n suppressClickUntil = Date.now() + 250;\n }\n return;\n }\n if (panMode) {\n panMode = false;\n panOrigin = null;\n target.classList.remove('is-panning');\n }\n });\n\n target.addEventListener('pointercancel', () => {\n if (dragNodeId) {\n pinnedIds.delete(dragNodeId);\n dragNodeId = null;\n wake();\n }\n panMode = false;\n panOrigin = null;\n target.classList.remove('is-panning');\n });\n }\n\n function matches(node) {\n const query = lower(searchInput?.value?.trim());\n const pathQuery = lower(pathInput?.value?.trim());\n const statusQuery = lower(statusSelect?.value?.trim());\n const tagQuery = lower(tagSelect?.value?.trim());\n if (hideExternal && node.isExternal) return false;\n if (hideIdeasets && node.kind === 'ideaset') return false;\n const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');\n if (query && !haystack.includes(query)) return false;\n if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;\n if (statusQuery && lower(node.status || '') !== statusQuery) return false;\n if (tagQuery && !(node.tags || []).some(tag => lower(tag) === tagQuery)) return false;\n return true;\n }\n\n function refresh() {\n const visibleNodes = (graph.nodes || []).filter(matches);\n const visibleIds = new Set(visibleNodes.map(node => node.id));\n const visibleEdges = (graph.edges || []).filter(edge =>\n visibleIds.has(edge.sourceId) && visibleIds.has(edge.targetId)\n );\n mountGraph(visibleNodes, visibleEdges);\n }\n\n for (const control of [searchInput, pathInput, statusSelect, tagSelect]) {\n control?.addEventListener('input', refresh);\n control?.addEventListener('change', refresh);\n }\n toggleExternal?.addEventListener('click', () => {\n hideExternal = !hideExternal;\n toggleExternal.classList.toggle('is-active', hideExternal);\n toggleExternal.textContent = hideExternal ? 'Show external' : 'Hide external';\n refresh();\n });\n toggleIdeasets?.addEventListener('click', () => {\n hideIdeasets = !hideIdeasets;\n toggleIdeasets.classList.toggle('is-active', hideIdeasets);\n toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';\n refresh();\n });\n togglePhysics?.addEventListener('click', () => {\n livePhysics = !livePhysics;\n togglePhysics.classList.toggle('is-active', livePhysics);\n togglePhysics.setAttribute('aria-pressed', livePhysics ? 'true' : 'false');\n if (livePhysics) {\n wake();\n } else {\n stopLoop();\n }\n });\n fitButton?.addEventListener('click', () => {\n fitView(true);\n });\n resetButton?.addEventListener('click', () => {\n if (searchInput) searchInput.value = '';\n if (pathInput) pathInput.value = '';\n if (statusSelect) statusSelect.value = '';\n if (tagSelect) tagSelect.value = '';\n hideExternal = false;\n hideIdeasets = false;\n toggleExternal?.classList.remove('is-active');\n toggleIdeasets?.classList.remove('is-active');\n if (toggleExternal) toggleExternal.textContent = 'Hide external';\n if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';\n positions.clear();\n velocities.clear();\n pinnedIds.clear();\n userViewport = false;\n refresh();\n });\n refresh();\n }\n}\n\nfunction boot() {\n const root = document.body;\n wireTables(root);\n wireGraph(root);\n void wireGlobalSearch(root);\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', boot);\n} else {\n boot();\n}\n";
3
3
  export declare function buildSearchIndexScript(documents: unknown): string;
@@ -704,11 +704,13 @@ function wireGraph(root) {
704
704
  const statusSelect = controls.querySelector('[data-graph-status]');
705
705
  const tagSelect = controls.querySelector('[data-graph-tag]');
706
706
  const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
707
+ const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');
707
708
  const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
708
709
  const fitButton = controls.querySelector('[data-graph-fit]');
709
710
  const resetButton = controls.querySelector('[data-graph-reset]');
710
711
  const statusText = controls.querySelector('[data-graph-status-text]');
711
712
  let hideExternal = false;
713
+ let hideIdeasets = false;
712
714
  let livePhysics = false;
713
715
  let animationFrame = 0;
714
716
  let calmTicks = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"html-export-assets.js","sourceRoot":"","sources":["../../src/export/html-export-assets.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgY5B,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA80BrB,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,SAAkB;IACrD,OAAO,wCAAwC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC;AAC3G,CAAC"}
1
+ {"version":3,"file":"html-export-assets.js","sourceRoot":"","sources":["../../src/export/html-export-assets.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgY5B,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAg1BrB,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,SAAkB;IACrD,OAAO,wCAAwC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC;AAC3G,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reqlan/analytical",
3
3
  "description": "Shared requirement graph index and analysers for reqlan",
4
- "version": "0.5.0",
4
+ "version": "0.5.4",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
7
7
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "langium": "~4.3.0",
42
42
  "sql.js": "^1.14.1",
43
43
  "zustand": "~5.0.8",
44
- "@reqlan/language": "1.5.0"
44
+ "@reqlan/language": "1.5.3"
45
45
  },
46
46
  "scripts": {
47
47
  "clean": "shx rm -fr *.tsbuildinfo out",