@reqlan/analytical 0.4.1 → 0.5.0
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 +12 -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 +187 -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 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.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
|
|
|
@@ -854,6 +1013,7 @@ function wireGraph(root) {
|
|
|
854
1013
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
855
1014
|
const classes = ['node'];
|
|
856
1015
|
if (node.isExternal) classes.push('external');
|
|
1016
|
+
if (node.kind === 'ideaset') classes.push('ideaset');
|
|
857
1017
|
if (node.isSubject || node.id === graph.centerId) classes.push('subject');
|
|
858
1018
|
group.setAttribute('class', classes.join(' '));
|
|
859
1019
|
group.dataset.nodeId = node.id;
|
|
@@ -989,6 +1149,7 @@ function wireGraph(root) {
|
|
|
989
1149
|
const statusQuery = lower(statusSelect?.value?.trim());
|
|
990
1150
|
const tagQuery = lower(tagSelect?.value?.trim());
|
|
991
1151
|
if (hideExternal && node.isExternal) return false;
|
|
1152
|
+
if (hideIdeasets && node.kind === 'ideaset') return false;
|
|
992
1153
|
const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
|
|
993
1154
|
if (query && !haystack.includes(query)) return false;
|
|
994
1155
|
if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;
|
|
@@ -1016,6 +1177,12 @@ function wireGraph(root) {
|
|
|
1016
1177
|
toggleExternal.textContent = hideExternal ? 'Show external' : 'Hide external';
|
|
1017
1178
|
refresh();
|
|
1018
1179
|
});
|
|
1180
|
+
toggleIdeasets?.addEventListener('click', () => {
|
|
1181
|
+
hideIdeasets = !hideIdeasets;
|
|
1182
|
+
toggleIdeasets.classList.toggle('is-active', hideIdeasets);
|
|
1183
|
+
toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
|
|
1184
|
+
refresh();
|
|
1185
|
+
});
|
|
1019
1186
|
togglePhysics?.addEventListener('click', () => {
|
|
1020
1187
|
livePhysics = !livePhysics;
|
|
1021
1188
|
togglePhysics.classList.toggle('is-active', livePhysics);
|
|
@@ -1035,8 +1202,11 @@ function wireGraph(root) {
|
|
|
1035
1202
|
if (statusSelect) statusSelect.value = '';
|
|
1036
1203
|
if (tagSelect) tagSelect.value = '';
|
|
1037
1204
|
hideExternal = false;
|
|
1205
|
+
hideIdeasets = false;
|
|
1038
1206
|
toggleExternal?.classList.remove('is-active');
|
|
1207
|
+
toggleIdeasets?.classList.remove('is-active');
|
|
1039
1208
|
if (toggleExternal) toggleExternal.textContent = 'Hide external';
|
|
1209
|
+
if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
|
|
1040
1210
|
positions.clear();
|
|
1041
1211
|
velocities.clear();
|
|
1042
1212
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA80BrB,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,SAAkB;IACrD,OAAO,wCAAwC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC;AAC3G,CAAC"}
|
|
@@ -1,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;
|