diff --git a/README.md b/README.md index 5b48313..5f9170c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Set `TOKEN=...` in the environment to pin your own. | Route | What it does | |-------------|-----------------------------------------------------------| | `GET /` | The editor page (no auth) | -| `GET /file` | Current strings file + git dirty/clean banner | +| `GET /file` | Current strings file + git dirty/clean banner + section list | | `POST /save`| Write the file, then `plutil -lint` it | | `POST /save-build` | Write the file and start a background build job | | `GET /job/` | Job progress (polled by the page, 2 s) | diff --git a/editor.py b/editor.py index 0c425e9..41fa1b4 100644 --- a/editor.py +++ b/editor.py @@ -20,6 +20,7 @@ Config via env: FUELBOARD_REPO, TOKEN, PORT (default 8790). import json import os +import re import secrets import shutil import subprocess @@ -76,6 +77,18 @@ def git_head() -> str: return git("log", "--oneline", "-1") or "no commits" +def parse_sections(content: str) -> list: + """Find single-line comment banners like /* Stations tab */ so the + editor can offer a jump-to-section dropdown. The multi-line header + comment at the top never matches (it doesn't end with */ on one line).""" + sections = [] + for i, line in enumerate(content.splitlines(), start=1): + m = re.match(r"^/\* (.+) \*/$", line.strip()) + if m: + sections.append({"name": m.group(1).strip(), "line": i}) + return sections + + # -------------------------------------------------------------------------- # Build job (runs in a background thread so the phone doesn't wait on it) # -------------------------------------------------------------------------- @@ -212,7 +225,8 @@ class Handler(BaseHTTPRequestHandler): self._json(404, {"error": f"missing {STRINGS_FILE}"}) return self._json(200, {"content": content, "dirty": git_dirty_files(), - "head": git_head(), "repo": str(REPO)}) + "head": git_head(), "repo": str(REPO), + "sections": parse_sections(content)}) return if self.path.startswith("/job/"): if not self._authorized(): diff --git a/index.html b/index.html index 9b1dcca..f62cceb 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,11 @@ padding: 8px 10px; border-radius: 8px; font-size: 13px; margin-bottom: 12px; } #dirty.ok { background: #12331c; border-color: #1f7a3d; color: #7ee2a0; } + select { + width: 100%; margin-bottom: 12px; padding: 10px 12px; font-size: 15px; + background: #0d0f12; color: #e6e8eb; border: 1px solid #2a313a; + border-radius: 10px; -webkit-appearance: none; appearance: none; + } textarea { width: 100%; min-height: 46vh; resize: vertical; background: #0d0f12; color: #d7dce2; border: 1px solid #2a313a; @@ -51,6 +56,9 @@

⛽ FuelBoard Copy Editor

·
+
@@ -83,6 +91,10 @@ async function load() { document.getElementById("src").value = data.content; document.getElementById("repo").textContent = data.repo; document.getElementById("head").textContent = data.head; + const jump = document.getElementById("jump"); + jump.innerHTML = '' + + (data.sections || []).map(s => ``).join(""); + jump.disabled = !(data.sections || []).length; const dirty = document.getElementById("dirty"); if (data.dirty.length) { dirty.style.display = "block"; @@ -155,6 +167,21 @@ function enable() { document.getElementById("save").addEventListener("click", saveOnly); document.getElementById("saveBuild").addEventListener("click", saveAndBuild); +document.getElementById("jump").addEventListener("change", (e) => { + const line = parseInt(e.target.value, 10); + e.target.value = ""; // act as a "go" control, not a persistent selection + if (!line) return; + const ta = document.getElementById("src"); + const ls = ta.value.split("\n"); + let offset = 0; + for (let i = 0; i < line - 1; i++) offset += ls[i].length + 1; + const bannerLen = ls[line - 1].length; + const lh = parseFloat(getComputedStyle(ta).lineHeight); // e.g. 19.5 + const pad = parseFloat(getComputedStyle(ta).paddingTop); // 12 + ta.focus({ preventScroll: true }); + ta.scrollTop = Math.max(0, (line - 1) * lh - pad); + ta.setSelectionRange(offset, offset + bannerLen); // flash-highlight the banner +}); load();