diff --git a/scripts/generate_protected_files.ps1 b/scripts/generate_protected_files.ps1 new file mode 100644 index 0000000..c6780c3 --- /dev/null +++ b/scripts/generate_protected_files.ps1 @@ -0,0 +1,82 @@ +<# +Generate .protected-files automatically based on repo heuristics. +Run: pwsh .\scripts\generate_protected_files.ps1 +#> + +$repoRoot = (Get-Location).Path +$outFile = Join-Path $repoRoot ".protected-files" + +# Files to always include (exact names) +$exactFiles = @( + 'package.json', + 'package-lock.json', + 'openapi.yaml', + '.env', + '.env.*', + 'mcp.json', + 'README.md', + 'Cargo.toml' +) + +# Directories to include (as prefixes, trailing slash will be added) +$dirs = @( + 'specification', + 'docs', + 'src/backend', + 'src/frontend/components' +) + +$results = [System.Collections.Generic.List[string]]::new() + +# Exclude common generated or vendored directories +$excludePattern = '(?:^|/)(node_modules|target|dist|\.git)(?:/|$)' + +# Add exact files if they exist in repo root or anywhere +foreach ($name in $exactFiles) { + $matches = Get-ChildItem -Path $repoRoot -Recurse -Force -File -Filter $name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName -ErrorAction SilentlyContinue + foreach ($m in $matches) { + if ($m.StartsWith($repoRoot)) { $rel = $m.Substring($repoRoot.Length) } else { $rel = $m } + $rel = $rel.TrimStart('\','/') -replace '\\','/' + if (-not [string]::IsNullOrWhiteSpace($rel)) { + if ($rel -notmatch $excludePattern) { $results.Add($rel) } + } + } +} + +# Add directories as prefixes (with trailing slash) +foreach ($d in $dirs) { + $dirPath = Join-Path $repoRoot $d + if (Test-Path $dirPath) { + $prefix = ($d.TrimEnd('/','\\') + '/') -replace '\\','/' + if ($prefix -notmatch $excludePattern) { $results.Add($prefix) } + } +} + +# Heuristic: include top-level build/config files +$heuristic = @('tsconfig.json','yarn.lock','.github','LICENSE') +foreach ($h in $heuristic) { + $p = Join-Path $repoRoot $h + if (Test-Path $p) { + if ((Get-Item $p).PSIsContainer) { + $entry = ($h.TrimEnd('/','\\') + '/') -replace '\\','/' + if ($entry -notmatch $excludePattern) { $results.Add($entry) } + } else { + # only add file if not in excluded paths + $full = Join-Path $repoRoot $h + $rel = $full.Substring($repoRoot.Length).TrimStart('\\','/') -replace '\\','/' + if ($rel -notmatch $excludePattern) { $results.Add($h) } + } + } +} + +# Unique, sorted +$unique = $results | Sort-Object -Unique + +# Write header and contents +$header = @("# .protected-files (auto-generated)", "# Do NOT edit manually unless you know what you're doing.", "# To regenerate run: pwsh .\\scripts\\generate_protected_files.ps1", "") +$body = $unique + +$all = $header + $body +$all | Out-File -FilePath $outFile -Encoding UTF8 -Force + +Write-Host "Generated $outFile with $($body.Count) entries." \ No newline at end of file diff --git a/src/frontend/components/app-sidebar.js b/src/frontend/components/app-sidebar.js new file mode 100644 index 0000000..5e47ad6 --- /dev/null +++ b/src/frontend/components/app-sidebar.js @@ -0,0 +1,113 @@ +class AppSidebar extends HTMLElement { + connectedCallback() { + if (this._initialized) return; + this._initialized = true; + this.className = 'sidebar'; + this.innerHTML = ` +
+ `; + + // accordion behavior + const header = this.querySelector('.accordion-header'); + const content = this.querySelector('.accordion-content'); + if (header && content) { + header.addEventListener('click', () => { + const expanded = header.getAttribute('aria-expanded') === 'true'; + header.setAttribute('aria-expanded', String(!expanded)); + if (expanded) { + content.hidden = true; + header.querySelector('.accordion-toggle').textContent = '▸'; + } else { + content.hidden = false; + header.querySelector('.accordion-toggle').textContent = '▾'; + } + }); + } + + // Add a resizer handle to allow dragging to change sidebar width + const resizer = document.createElement('div'); + resizer.className = 'sidebar-resizer'; + this.appendChild(resizer); + + let isResizing = false; + let startX = 0; + let startWidth = 0; + + const onPointerMove = (e) => { + if (!isResizing) return; + const dx = e.clientX - startX; + const minW = 160; + const maxW = 520; + let newW = Math.max(minW, Math.min(maxW, startWidth + dx)); + document.documentElement.style.setProperty('--sidebar-width', `${newW}px`); + }; + + const onPointerUp = () => { + if (!isResizing) return; + isResizing = false; + document.body.style.userSelect = ''; + window.removeEventListener('pointermove', onPointerMove); + window.removeEventListener('pointerup', onPointerUp); + try { localStorage.setItem('telos_sidebar_width', getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width')); } catch (e) {} + }; + + resizer.addEventListener('pointerdown', (e) => { + isResizing = true; + startX = e.clientX; + const current = getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width').trim(); + startWidth = parseInt(current) || this.offsetWidth || 244; + document.body.style.userSelect = 'none'; + window.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', onPointerUp); + }); + + // Restore saved width from localStorage if present + try { + const saved = localStorage.getItem('telos_sidebar_width'); + if (saved) document.documentElement.style.setProperty('--sidebar-width', saved.trim()); + } catch (e) {} + + // Navigation: dispatch custom event when a nav item is clicked + const navButtons = this.querySelectorAll('.sidebar-nav .nav-item'); + navButtons.forEach(btn => { + btn.addEventListener('click', (ev) => { + const panel = btn.getAttribute('data-panel') || btn.textContent.trim(); + // toggle active class + navButtons.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + // dispatch event to document so main panel can listen + const e = new CustomEvent('navigate-panel', { detail: panel, bubbles: true }); + document.dispatchEvent(e); + }); + }); + } +} + +customElements.define('app-sidebar', AppSidebar); diff --git a/src/frontend/components/site-footer.js b/src/frontend/components/site-footer.js new file mode 100644 index 0000000..17afc29 --- /dev/null +++ b/src/frontend/components/site-footer.js @@ -0,0 +1,15 @@ +class SiteFooter extends HTMLElement { + connectedCallback() { + if (this._initialized) return; + this._initialized = true; + this.className = 'site-footer'; + this.innerHTML = ` + + `; + } +} + +customElements.define('site-footer', SiteFooter);