@reqlan/analytical 0.4.1 → 0.5.1
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 +18 -0
- package/README.template.md +1 -0
- package/out/analysis-api.d.ts +8 -0
- package/out/analysis-api.js +21 -0
- package/out/analysis-api.js.map +1 -1
- package/out/core/application-memory.d.ts +11 -0
- package/out/core/application-memory.js +26 -0
- package/out/core/application-memory.js.map +1 -0
- package/out/create-runtime.d.ts +1 -0
- package/out/create-runtime.js +2 -3
- package/out/create-runtime.js.map +1 -1
- package/out/export/build-export-snapshot.js +52 -30
- package/out/export/build-export-snapshot.js.map +1 -1
- package/out/export/export-html.d.ts +4 -0
- package/out/export/export-html.js +8 -0
- package/out/export/export-html.js.map +1 -0
- package/out/export/html-export-assets.d.ts +2 -2
- package/out/export/html-export-assets.js +189 -17
- package/out/export/html-export-assets.js.map +1 -1
- package/out/export/html-export-template.d.ts +2 -1
- package/out/export/html-export-template.js +230 -120
- package/out/export/html-export-template.js.map +1 -1
- package/out/export/html-export-utils.d.ts +13 -2
- package/out/export/html-export-utils.js +167 -52
- package/out/export/html-export-utils.js.map +1 -1
- package/out/export/secret-rq.d.ts +2 -0
- package/out/export/secret-rq.js +9 -0
- package/out/export/secret-rq.js.map +1 -0
- package/out/export/types.d.ts +25 -0
- package/out/export/write-html-export.js +7 -1
- package/out/export/write-html-export.js.map +1 -1
- package/out/headless-index-service.js +2 -1
- package/out/headless-index-service.js.map +1 -1
- package/out/index-store/idea-extractor.js +42 -7
- package/out/index-store/idea-extractor.js.map +1 -1
- package/out/index-store/webview-graph-queries.d.ts +12 -0
- package/out/index-store/webview-graph-queries.js +37 -3
- package/out/index-store/webview-graph-queries.js.map +1 -1
- package/out/index.d.ts +5 -2
- package/out/index.js +4 -1
- package/out/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,3 +1,3 @@
|
|
|
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 { max-width: 1360px; margin: 0 auto; padding: 1.5rem; }\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 1.5rem;\n padding: 0.9rem 1.5rem;\n}\n.topbar-inner, .hero, .split, .grid, .detail-grid { display: grid; gap: 1rem; }\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.hero { grid-template-columns: 2fr 1fr; margin-bottom: 1.5rem; }\n.grid { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }\n.split, .detail-grid { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }\n.panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result {\n background: var(--bg-soft);\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: 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 { margin-bottom: 1rem; }\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.55rem; 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-raised); 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-raised);\n}\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.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.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}\n.entity-list { display: grid; gap: 0.85rem; }\n.entity-card {\n border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg-raised);\n color: inherit;\n}\n.entity-card:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }\n.entity-card h3, .panel h2, .table-shell h2, .graph-shell h2 { margin-top: 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.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 .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 { margin-bottom: 0.65rem; }\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, .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 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\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 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 }\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 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\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.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 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 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 toggleExternal?.classList.remove('is-active');\n if (toggleExternal) toggleExternal.textContent = 'Hide external';\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";
|
|
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 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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\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;
|
|
@@ -28,13 +28,16 @@ body {
|
|
|
28
28
|
}
|
|
29
29
|
a { color: var(--accent-strong); text-decoration: none; }
|
|
30
30
|
a:hover { color: var(--accent); }
|
|
31
|
-
main.layout {
|
|
31
|
+
main.layout {
|
|
32
|
+
max-width: 1360px; margin: 0 auto; padding: 1.5rem;
|
|
33
|
+
display: flex; flex-direction: column; gap: 1.5rem;
|
|
34
|
+
}
|
|
32
35
|
.topbar {
|
|
33
36
|
position: sticky; top: 0; z-index: 10;
|
|
34
|
-
background: var(--bg-elev); border-bottom: 1px solid var(--line); margin: 0 -1.5rem
|
|
37
|
+
background: var(--bg-elev); border-bottom: 1px solid var(--line); margin: 0 -1.5rem;
|
|
35
38
|
padding: 0.9rem 1.5rem;
|
|
36
39
|
}
|
|
37
|
-
.topbar-inner, .hero, .split, .grid, .detail-grid { display: grid; gap:
|
|
40
|
+
.topbar-inner, .hero, .split, .grid, .detail-grid { display: grid; gap: 1.5rem; }
|
|
38
41
|
.topbar-inner { grid-template-columns: 1.3fr auto; align-items: center; }
|
|
39
42
|
.nav { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
|
|
40
43
|
.nav a {
|
|
@@ -46,11 +49,35 @@ main.layout { max-width: 1360px; margin: 0 auto; padding: 1.5rem; }
|
|
|
46
49
|
background: color-mix(in srgb, var(--rust) 28%, var(--bg-raised));
|
|
47
50
|
color: var(--fg); border-color: var(--rust);
|
|
48
51
|
}
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
.nav a.brand-link {
|
|
53
|
+
background: transparent;
|
|
54
|
+
border-color: transparent;
|
|
55
|
+
color: var(--fg);
|
|
56
|
+
font-weight: 650;
|
|
57
|
+
letter-spacing: 0.02em;
|
|
58
|
+
padding-left: 0.15rem;
|
|
59
|
+
padding-right: 0.9rem;
|
|
60
|
+
}
|
|
61
|
+
.nav a.brand-link:hover {
|
|
62
|
+
color: var(--accent);
|
|
63
|
+
border-color: transparent;
|
|
64
|
+
background: transparent;
|
|
65
|
+
}
|
|
66
|
+
.hero { grid-template-columns: 2fr 1fr; }
|
|
67
|
+
.grid {
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
|
|
70
|
+
gap: 1.5rem;
|
|
71
|
+
row-gap: 1.5rem;
|
|
72
|
+
column-gap: 1.5rem;
|
|
73
|
+
}
|
|
74
|
+
.split, .detail-grid {
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
|
77
|
+
gap: 1.5rem;
|
|
78
|
+
}
|
|
52
79
|
.panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result {
|
|
53
|
-
background: var(--bg-
|
|
80
|
+
background: var(--bg-raised);
|
|
54
81
|
border: 1px solid var(--line);
|
|
55
82
|
border-radius: 10px;
|
|
56
83
|
}
|
|
@@ -59,20 +86,23 @@ main.layout { max-width: 1360px; margin: 0 auto; padding: 1.5rem; }
|
|
|
59
86
|
.metric-link, .metric > span, .metric > strong {
|
|
60
87
|
display: flex; flex-direction: column; gap: 0.35rem; padding: 1rem; color: inherit;
|
|
61
88
|
}
|
|
62
|
-
.metric-link:hover { background: var(--bg-raised); }
|
|
89
|
+
.metric-link:hover { background: color-mix(in srgb, var(--accent) 10%, var(--bg-raised)); }
|
|
63
90
|
.metric-label, .subtle, .eyebrow, .breadcrumbs { color: var(--muted); }
|
|
64
91
|
.metric-value { font-size: 1.75rem; color: var(--accent-strong); }
|
|
65
92
|
.eyebrow { text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.75rem; color: var(--rust); }
|
|
66
|
-
.page-header {
|
|
93
|
+
.page-header { display: grid; gap: 0.55rem; }
|
|
94
|
+
.page-header h1 { margin: 0; }
|
|
95
|
+
.page-header .eyebrow { margin: 0; }
|
|
96
|
+
.page-header .subtle { margin: 0; }
|
|
67
97
|
.toolbar {
|
|
68
98
|
display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; justify-content: space-between;
|
|
69
99
|
margin-bottom: 0.9rem;
|
|
70
100
|
}
|
|
71
|
-
.toolbar .actions, .pill-row, .breadcrumbs { display: flex; gap: 0.
|
|
101
|
+
.toolbar .actions, .pill-row, .breadcrumbs { display: flex; gap: 0.75rem; row-gap: 0.85rem; flex-wrap: wrap; align-items: center; }
|
|
72
102
|
.chip, .pill {
|
|
73
103
|
display: inline-flex; align-items: center; gap: 0.35rem;
|
|
74
104
|
padding: 0.28rem 0.6rem; border-radius: 8px;
|
|
75
|
-
border: 1px solid var(--line); background: var(--bg
|
|
105
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--muted);
|
|
76
106
|
}
|
|
77
107
|
a.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
|
|
78
108
|
.graph-controls-bar {
|
|
@@ -84,8 +114,9 @@ a.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var
|
|
|
84
114
|
padding: 0.8rem;
|
|
85
115
|
border: 1px solid var(--line);
|
|
86
116
|
border-radius: 10px;
|
|
87
|
-
background: var(--bg
|
|
117
|
+
background: var(--bg);
|
|
88
118
|
}
|
|
119
|
+
.graph-shell > .pill-row { margin-bottom: 0.9rem; }
|
|
89
120
|
.graph-filter {
|
|
90
121
|
flex: 1 1 180px;
|
|
91
122
|
min-width: 160px;
|
|
@@ -125,6 +156,23 @@ th, td {
|
|
|
125
156
|
th { color: var(--muted); font-size: 0.94rem; }
|
|
126
157
|
tbody tr:hover { background: var(--bg-raised); }
|
|
127
158
|
.table-shell { overflow-x: auto; }
|
|
159
|
+
.scroll-window {
|
|
160
|
+
--scroll-window-max: min(28rem, 50vh);
|
|
161
|
+
}
|
|
162
|
+
body[data-runtime-mode="interactive"] .scroll-window {
|
|
163
|
+
max-height: var(--scroll-window-max);
|
|
164
|
+
overflow: auto;
|
|
165
|
+
border: 1px solid var(--line);
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
background: var(--bg);
|
|
168
|
+
}
|
|
169
|
+
body[data-runtime-mode="interactive"] .scroll-window > table {
|
|
170
|
+
margin: 0;
|
|
171
|
+
}
|
|
172
|
+
body[data-runtime-mode="interactive"] .scroll-window .entity-list,
|
|
173
|
+
body[data-runtime-mode="interactive"] .scroll-window .rollup-list {
|
|
174
|
+
padding: 0.75rem;
|
|
175
|
+
}
|
|
128
176
|
.sortable-th { padding: 0.45rem 0.4rem; }
|
|
129
177
|
.sort-button {
|
|
130
178
|
display: inline-flex; align-items: center; gap: 0.35rem;
|
|
@@ -136,6 +184,29 @@ tbody tr:hover { background: var(--bg-raised); }
|
|
|
136
184
|
.sort-button.sort-active { color: var(--accent-strong); }
|
|
137
185
|
.sort-indicator { color: var(--faint); font-size: 0.8em; min-width: 1em; }
|
|
138
186
|
.sort-button.sort-active .sort-indicator { color: var(--accent-strong); }
|
|
187
|
+
.table-filter-toggle {
|
|
188
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
189
|
+
width: 2.25rem; height: 2.25rem; padding: 0;
|
|
190
|
+
border: 1px solid var(--line); border-radius: 8px;
|
|
191
|
+
background: var(--bg-raised); color: var(--muted); cursor: pointer;
|
|
192
|
+
}
|
|
193
|
+
.table-filter-toggle:hover { color: var(--fg); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
|
|
194
|
+
.table-filter-toggle.is-active {
|
|
195
|
+
color: var(--fg);
|
|
196
|
+
border-color: var(--accent);
|
|
197
|
+
background: color-mix(in srgb, var(--accent) 18%, var(--bg-raised));
|
|
198
|
+
}
|
|
199
|
+
.table-filter-toggle.has-filters:not(.is-active) {
|
|
200
|
+
color: var(--accent-strong);
|
|
201
|
+
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
|
202
|
+
}
|
|
203
|
+
.table-filter-toggle svg { width: 1rem; height: 1rem; display: block; }
|
|
204
|
+
.table-filter-actions {
|
|
205
|
+
display: flex; justify-content: flex-end; gap: 0.55rem;
|
|
206
|
+
margin-bottom: 0.65rem;
|
|
207
|
+
}
|
|
208
|
+
.column-filter-row { display: none; }
|
|
209
|
+
.column-filter-row.is-open { display: table-row; }
|
|
139
210
|
.column-filter-row th {
|
|
140
211
|
padding: 0.35rem 0.4rem 0.75rem; border-bottom: 1px solid var(--line); font-weight: 400;
|
|
141
212
|
}
|
|
@@ -148,18 +219,50 @@ tbody tr:hover { background: var(--bg-raised); }
|
|
|
148
219
|
border-color: var(--accent);
|
|
149
220
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);
|
|
150
221
|
}
|
|
151
|
-
.
|
|
222
|
+
body[data-runtime-mode="interactive"] .scroll-window thead tr:first-child th {
|
|
223
|
+
position: sticky;
|
|
224
|
+
top: 0;
|
|
225
|
+
z-index: 2;
|
|
226
|
+
background: var(--bg-raised);
|
|
227
|
+
box-shadow: 0 1px 0 var(--line);
|
|
228
|
+
}
|
|
229
|
+
body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is-open th {
|
|
230
|
+
position: sticky;
|
|
231
|
+
top: 2.65rem;
|
|
232
|
+
z-index: 1;
|
|
233
|
+
background: var(--bg-raised);
|
|
234
|
+
box-shadow: 0 1px 0 var(--line);
|
|
235
|
+
}
|
|
236
|
+
.entity-list { display: grid; gap: 1.5rem; }
|
|
152
237
|
.entity-card {
|
|
153
|
-
border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg
|
|
238
|
+
border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg);
|
|
154
239
|
color: inherit;
|
|
155
240
|
}
|
|
156
241
|
.entity-card:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
|
|
157
|
-
.entity-card h3, .
|
|
242
|
+
.entity-card h3, .entity-card p { margin: 0; }
|
|
243
|
+
.entity-card p + p, .entity-card strong + p, .entity-card h3 + p { margin-top: 0.35rem; }
|
|
244
|
+
.panel > h2, .table-shell > h2, .graph-shell > h2, .print-card > h3 { margin-top: 0; margin-bottom: 0.75rem; }
|
|
245
|
+
.toolbar h2 { margin: 0; }
|
|
158
246
|
.rollup-list { display: grid; gap: 0.5rem; }
|
|
159
247
|
.rollup-list div {
|
|
160
248
|
display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--line); padding-bottom: 0.4rem;
|
|
161
249
|
}
|
|
162
250
|
.rollup-list dd { margin: 0; }
|
|
251
|
+
.distribution-track {
|
|
252
|
+
height: 0.7rem;
|
|
253
|
+
min-width: 6rem;
|
|
254
|
+
border-radius: 999px;
|
|
255
|
+
background: var(--bg);
|
|
256
|
+
border: 1px solid var(--line);
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
}
|
|
259
|
+
.distribution-fill {
|
|
260
|
+
display: block;
|
|
261
|
+
height: 100%;
|
|
262
|
+
border-radius: inherit;
|
|
263
|
+
background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));
|
|
264
|
+
min-width: 0;
|
|
265
|
+
}
|
|
163
266
|
.graph-root svg {
|
|
164
267
|
width: 100%; min-height: 620px; background: var(--bg); border-radius: 10px; border: 1px solid var(--line);
|
|
165
268
|
}
|
|
@@ -173,6 +276,7 @@ tbody tr:hover { background: var(--bg-raised); }
|
|
|
173
276
|
transition: fill 180ms ease, stroke 180ms ease, r 180ms ease;
|
|
174
277
|
}
|
|
175
278
|
.graph-root .external circle { fill: var(--rust-muted); stroke: color-mix(in srgb, var(--rust) 50%, transparent); }
|
|
279
|
+
.graph-root .ideaset circle { fill: color-mix(in srgb, #d18616 78%, white); stroke: #d18616; }
|
|
176
280
|
.graph-root .subject circle {
|
|
177
281
|
fill: var(--rust);
|
|
178
282
|
stroke: var(--accent-strong);
|
|
@@ -212,7 +316,6 @@ tbody tr:hover { background: var(--bg-raised); }
|
|
|
212
316
|
.search-results { display: grid; gap: 0.7rem; margin-top: 1rem; }
|
|
213
317
|
.search-result:hover { border-color: var(--accent-dim); }
|
|
214
318
|
.hidden { display: none !important; }
|
|
215
|
-
.breadcrumbs { margin-bottom: 0.65rem; }
|
|
216
319
|
.breadcrumbs a { color: var(--muted); }
|
|
217
320
|
.breadcrumbs a:hover { color: var(--accent-strong); }
|
|
218
321
|
.section-link { font-size: 0.92rem; }
|
|
@@ -261,11 +364,21 @@ pre.code-like {
|
|
|
261
364
|
@media print {
|
|
262
365
|
:root { color-scheme: light; }
|
|
263
366
|
body { background: white; color: black; }
|
|
264
|
-
.topbar, .searchbar, .toolbar .actions, .graph-shell .toolbar, .column-filter-row, .hide-on-print { display: none !important; }
|
|
367
|
+
.topbar, .searchbar, .toolbar .actions, .graph-shell .toolbar, .column-filter-row, .table-filter-toggle, .table-filter-actions, .hide-on-print { display: none !important; }
|
|
265
368
|
.sort-button { cursor: default; }
|
|
266
369
|
.panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result, .entity-card {
|
|
267
370
|
background: white; color: black; border-color: #cbd5e1; box-shadow: none;
|
|
268
371
|
}
|
|
372
|
+
.scroll-window {
|
|
373
|
+
max-height: none !important;
|
|
374
|
+
overflow: visible !important;
|
|
375
|
+
border: none !important;
|
|
376
|
+
background: transparent !important;
|
|
377
|
+
}
|
|
378
|
+
.scroll-window thead th {
|
|
379
|
+
position: static !important;
|
|
380
|
+
box-shadow: none !important;
|
|
381
|
+
}
|
|
269
382
|
a { color: black; text-decoration: underline; }
|
|
270
383
|
.print-break-avoid { break-inside: avoid; }
|
|
271
384
|
}
|
|
@@ -377,6 +490,44 @@ function isPlaceholderRow(row) {
|
|
|
377
490
|
return [...row.cells].some(cell => Number(cell.colSpan || 1) > 1);
|
|
378
491
|
}
|
|
379
492
|
|
|
493
|
+
const 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>';
|
|
494
|
+
|
|
495
|
+
function ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState) {
|
|
496
|
+
const shell = table.closest('.table-shell') || table.parentElement;
|
|
497
|
+
const toolbar = shell ? shell.querySelector(':scope > .toolbar') : null;
|
|
498
|
+
let actions = toolbar ? toolbar.querySelector('.actions') : null;
|
|
499
|
+
if (toolbar && !actions) {
|
|
500
|
+
actions = document.createElement('div');
|
|
501
|
+
actions.className = 'actions';
|
|
502
|
+
toolbar.appendChild(actions);
|
|
503
|
+
}
|
|
504
|
+
if (!actions) {
|
|
505
|
+
actions = document.createElement('div');
|
|
506
|
+
actions.className = 'actions table-filter-actions';
|
|
507
|
+
table.parentElement?.insertBefore(actions, table);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const toggle = document.createElement('button');
|
|
511
|
+
toggle.type = 'button';
|
|
512
|
+
toggle.className = 'table-filter-toggle';
|
|
513
|
+
toggle.setAttribute('aria-label', 'Toggle column filters');
|
|
514
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
515
|
+
toggle.setAttribute('title', 'Column filters');
|
|
516
|
+
toggle.innerHTML = FILTER_ICON_SVG;
|
|
517
|
+
toggle.addEventListener('click', () => {
|
|
518
|
+
const open = !filterRow.classList.contains('is-open');
|
|
519
|
+
filterRow.classList.toggle('is-open', open);
|
|
520
|
+
toggle.classList.toggle('is-active', open);
|
|
521
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
522
|
+
syncToggleState();
|
|
523
|
+
if (open) {
|
|
524
|
+
filterInputs[0]?.focus();
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
actions.appendChild(toggle);
|
|
528
|
+
return toggle;
|
|
529
|
+
}
|
|
530
|
+
|
|
380
531
|
function wireTables(root) {
|
|
381
532
|
const tables = root.querySelectorAll('table');
|
|
382
533
|
const tableControllers = [];
|
|
@@ -395,6 +546,12 @@ function wireTables(root) {
|
|
|
395
546
|
let sortIndex = -1;
|
|
396
547
|
let sortDir = 'asc';
|
|
397
548
|
const filterInputs = [];
|
|
549
|
+
let filterToggle = null;
|
|
550
|
+
const syncToggleState = () => {
|
|
551
|
+
if (!filterToggle) return;
|
|
552
|
+
const hasFilters = filterInputs.some(input => input.value.trim());
|
|
553
|
+
filterToggle.classList.toggle('has-filters', hasFilters);
|
|
554
|
+
};
|
|
398
555
|
const controller = {
|
|
399
556
|
table,
|
|
400
557
|
tbody,
|
|
@@ -417,6 +574,7 @@ function wireTables(root) {
|
|
|
417
574
|
});
|
|
418
575
|
row.classList.toggle('hidden', !(matchesGlobal && matchesColumns));
|
|
419
576
|
}
|
|
577
|
+
syncToggleState();
|
|
420
578
|
}
|
|
421
579
|
};
|
|
422
580
|
|
|
@@ -482,6 +640,7 @@ function wireTables(root) {
|
|
|
482
640
|
filterRow.appendChild(cell);
|
|
483
641
|
});
|
|
484
642
|
thead.appendChild(filterRow);
|
|
643
|
+
filterToggle = ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState);
|
|
485
644
|
tableControllers.push(controller);
|
|
486
645
|
}
|
|
487
646
|
|
|
@@ -545,11 +704,13 @@ function wireGraph(root) {
|
|
|
545
704
|
const statusSelect = controls.querySelector('[data-graph-status]');
|
|
546
705
|
const tagSelect = controls.querySelector('[data-graph-tag]');
|
|
547
706
|
const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
|
|
707
|
+
const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');
|
|
548
708
|
const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
|
|
549
709
|
const fitButton = controls.querySelector('[data-graph-fit]');
|
|
550
710
|
const resetButton = controls.querySelector('[data-graph-reset]');
|
|
551
711
|
const statusText = controls.querySelector('[data-graph-status-text]');
|
|
552
712
|
let hideExternal = false;
|
|
713
|
+
let hideIdeasets = false;
|
|
553
714
|
let livePhysics = false;
|
|
554
715
|
let animationFrame = 0;
|
|
555
716
|
let calmTicks = 0;
|
|
@@ -854,6 +1015,7 @@ function wireGraph(root) {
|
|
|
854
1015
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
855
1016
|
const classes = ['node'];
|
|
856
1017
|
if (node.isExternal) classes.push('external');
|
|
1018
|
+
if (node.kind === 'ideaset') classes.push('ideaset');
|
|
857
1019
|
if (node.isSubject || node.id === graph.centerId) classes.push('subject');
|
|
858
1020
|
group.setAttribute('class', classes.join(' '));
|
|
859
1021
|
group.dataset.nodeId = node.id;
|
|
@@ -989,6 +1151,7 @@ function wireGraph(root) {
|
|
|
989
1151
|
const statusQuery = lower(statusSelect?.value?.trim());
|
|
990
1152
|
const tagQuery = lower(tagSelect?.value?.trim());
|
|
991
1153
|
if (hideExternal && node.isExternal) return false;
|
|
1154
|
+
if (hideIdeasets && node.kind === 'ideaset') return false;
|
|
992
1155
|
const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
|
|
993
1156
|
if (query && !haystack.includes(query)) return false;
|
|
994
1157
|
if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;
|
|
@@ -1016,6 +1179,12 @@ function wireGraph(root) {
|
|
|
1016
1179
|
toggleExternal.textContent = hideExternal ? 'Show external' : 'Hide external';
|
|
1017
1180
|
refresh();
|
|
1018
1181
|
});
|
|
1182
|
+
toggleIdeasets?.addEventListener('click', () => {
|
|
1183
|
+
hideIdeasets = !hideIdeasets;
|
|
1184
|
+
toggleIdeasets.classList.toggle('is-active', hideIdeasets);
|
|
1185
|
+
toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
|
|
1186
|
+
refresh();
|
|
1187
|
+
});
|
|
1019
1188
|
togglePhysics?.addEventListener('click', () => {
|
|
1020
1189
|
livePhysics = !livePhysics;
|
|
1021
1190
|
togglePhysics.classList.toggle('is-active', livePhysics);
|
|
@@ -1035,8 +1204,11 @@ function wireGraph(root) {
|
|
|
1035
1204
|
if (statusSelect) statusSelect.value = '';
|
|
1036
1205
|
if (tagSelect) tagSelect.value = '';
|
|
1037
1206
|
hideExternal = false;
|
|
1207
|
+
hideIdeasets = false;
|
|
1038
1208
|
toggleExternal?.classList.remove('is-active');
|
|
1209
|
+
toggleIdeasets?.classList.remove('is-active');
|
|
1039
1210
|
if (toggleExternal) toggleExternal.textContent = 'Hide external';
|
|
1211
|
+
if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
|
|
1040
1212
|
positions.clear();
|
|
1041
1213
|
velocities.clear();
|
|
1042
1214
|
pinnedIds.clear();
|
|
@@ -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
|
|
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"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ExportClusterRecord, ExportCodeFileRecord, ExportFileRecord, ExportIdeaRecord, ExportSnapshot } from './types.js';
|
|
1
|
+
import type { ExportAttributeRecord, ExportClusterRecord, ExportCodeFileRecord, ExportFileRecord, ExportIdeaRecord, ExportSnapshot } from './types.js';
|
|
2
2
|
export declare function renderHomePage(snapshot: ExportSnapshot): string;
|
|
3
3
|
export declare function renderIdeasIndexPage(snapshot: ExportSnapshot): string;
|
|
4
4
|
export declare function renderFilesIndexPage(snapshot: ExportSnapshot): string;
|
|
5
5
|
export declare function renderCodeFilesIndexPage(snapshot: ExportSnapshot): string;
|
|
6
6
|
export declare function renderClustersIndexPage(snapshot: ExportSnapshot): string;
|
|
7
7
|
export declare function renderAttributesIndexPage(snapshot: ExportSnapshot): string;
|
|
8
|
+
export declare function renderAttributeDetailPage(snapshot: ExportSnapshot, attribute: ExportAttributeRecord): string;
|
|
8
9
|
export declare function renderGraphPage(snapshot: ExportSnapshot): string;
|
|
9
10
|
export declare function renderIdeaDetailPage(snapshot: ExportSnapshot, idea: ExportIdeaRecord): string;
|
|
10
11
|
export declare function renderFileDetailPage(snapshot: ExportSnapshot, file: ExportFileRecord): string;
|