Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ E2E tests (`tests/e2e/board.spec.js`) use Playwright with Chromium. They inject
## Architecture

The entire application lives in `index.html`:
- **Lines 7–202**: CSS — 4 themes (`dark`, `light`, `ocean`, `amber`) defined via CSS custom properties on `:root` and `[data-theme="..."]` selectors. Theme is persisted to `localStorage` under key `mb-theme`.
- **Lines 7–202**: CSS — 4 built-in themes (`dark`, `light`, `ocean`, `amber`) defined via CSS custom properties on `:root` and `[data-theme="..."]` selectors. Theme is persisted to `localStorage` under key `mb-theme`. A fifth `custom` theme can be loaded at runtime via the CSS plugin system (see Key Functions below).
- **Lines 203–265**: HTML structure — `#tab-bar`, `#connect-screen`, `#board` (phases, stats, progress bar, notes), `#toast`.
- **Lines 267–675**: Vanilla JS — no frameworks, no imports.

Expand All @@ -62,6 +62,9 @@ DRAG / DRAG_OVER // drag-and-drop transient state
| `saveBoard(idx?)` | Writes `serialiseMD(data)` back to the file via the File System Access API |
| `cycleFeature(pi, fi)` | Cycles a feature's status (`pending → active → done → pending`), auto-derives phase status, then saves |
| `setTheme(t)` | Applies `data-theme` attribute and persists to `localStorage` |
| `pickCustomTheme()` | File picker for a `.css` plugin → `loadCustomTheme` |
| `loadCustomTheme(css)` | Injects CSS into `<style id="mb-custom-theme">`, saves to `localStorage`, activates `custom` theme |
| `clearCustomTheme()` | Removes injected CSS, clears `localStorage`, resets to dark |

Auto-save triggers on every status change and after a 1 s debounce on notes input.

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ npm run test:all # Both

## Making changes

- **CSS**: Lines 7–200 of `index.html`. Themes use CSS custom properties — update all four theme blocks if adding a new variable.
- **CSS**: Lines 7–200 of `index.html`. The four built-in themes use CSS custom properties — update all four theme blocks if adding a new variable. Custom user themes are loaded at runtime via `loadCustomTheme()` and stored in `localStorage`; see the **Custom themes** section in the README.
- **HTML**: Lines 203–265. Keep it minimal.
- **JS**: Lines 267–675. No imports, no modules — everything is in the global scope.

Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,43 @@ The `<!-- meta: key=value ... -->` comment is optional. Recognised keys:
- **Click to cycle** — click any feature to cycle pending → active → done
- **Drag to reorder** — drag features within a phase, or drag phases to reorder
- **Due dates** — colour-coded overdue/soon/future badges
- **4 themes** — Dark, Light, Ocean, Amber (remembered across sessions)
- **5 themes** — Dark, Light, Ocean, Amber, plus **custom CSS plugin** (load any `.css` file)
- **Auto-save** — every status change writes back to the `.md` file immediately
- **Notes field** — free-text area saved to the `## Notes` section
- **Zero dependencies** — single HTML file, works offline

---

## Custom themes

Click **+ CSS** in the theme bar and pick any `.css` file. The file is injected as a stylesheet and remembered across sessions — no re-picking needed on reload.

The plugin format is a single CSS block targeting `[data-theme="custom"]`:

```css
/* my-theme.css */
[data-theme="custom"] {
--bg: #1e1e2e;
--surface: #181825;
--surface2: #313244;
--border: #45475a;
--accent: #a6e3a1;
--accent-dim: #1e3a2e;
--accent2: #fab387;
--accent2-dim: #3d2010;
--blue: #89b4fa;
--red: #f38ba8;
--muted: #6c7086;
--text: #cdd6f4;
--text-dim: #a6adc8;
--radius: 10px;
}
```

Any omitted variables fall back to the dark theme. You can also include arbitrary CSS rules to customise individual components. To reset, call `clearCustomTheme()` in the browser console.

---

## Requirements

Chrome or Edge 86+ (uses the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API) to read and write local files). Firefox does not support this API.
Expand Down
46 changes: 46 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
--text: #fef3c7; --text-dim: #d97706;
}

.theme-btn-custom { border-style: dashed; }

/* ── Reset & base ───────────────────────────────────────────────────────── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
Expand Down Expand Up @@ -228,6 +230,8 @@
<button class="theme-btn" data-theme="light" onclick="setTheme('light')">Light</button>
<button class="theme-btn" data-theme="ocean" onclick="setTheme('ocean')">Ocean</button>
<button class="theme-btn" data-theme="amber" onclick="setTheme('amber')">Amber</button>
<button class="theme-btn theme-btn-custom" id="custom-theme-btn" data-theme="custom" onclick="setTheme('custom')" style="display:none">Custom</button>
<button class="theme-btn" id="load-css-btn" onclick="pickCustomTheme()" title="Load a .css theme file">+ CSS</button>
</div>
</div>

Expand Down Expand Up @@ -724,6 +728,44 @@ <h3>Notes</h3>
localStorage.setItem("mb-theme", t);
}

// ── Custom theme plugin ────────────────────────────────────────────────────
function injectCustomCSS(cssText) {
let el = document.getElementById("mb-custom-theme");
if (!el) { el = document.createElement("style"); el.id = "mb-custom-theme"; document.head.appendChild(el); }
el.textContent = cssText;
}

function syncCustomBtn() {
const btn = document.getElementById("custom-theme-btn");
if (btn) btn.style.display = localStorage.getItem("mb-custom-css") ? "" : "none";
}

function loadCustomTheme(cssText) {
injectCustomCSS(cssText);
localStorage.setItem("mb-custom-css", cssText);
setTheme("custom");
syncCustomBtn();
}

async function pickCustomTheme() {
try {
const [fh] = await window.showOpenFilePicker({ types: [{ description: "CSS Theme", accept: { "text/css": [".css"] } }] });
const file = await fh.getFile();
const text = await file.text();
loadCustomTheme(text);
toast("Custom theme loaded · " + fh.name);
} catch(e) { if (e.name !== "AbortError") toast("Error: " + e.message); }
}

function clearCustomTheme() {
const el = document.getElementById("mb-custom-theme");
if (el) el.remove();
localStorage.removeItem("mb-custom-css");
setTheme("dark");
syncCustomBtn();
toast("Custom theme cleared");
}

// ── Utilities ──────────────────────────────────────────────────────────────
function esc(s) { return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }

Expand All @@ -741,8 +783,11 @@ <h3>Notes</h3>
});

// ── Init ───────────────────────────────────────────────────────────────────
const savedCSS = localStorage.getItem("mb-custom-css");
if (savedCSS) injectCustomCSS(savedCSS);
const savedTheme = localStorage.getItem("mb-theme") || "dark";
setTheme(savedTheme);
syncCustomBtn();

document.addEventListener("click", (e) => {
if (!e.target.closest("#tab-add")) closeTabMenu();
Expand All @@ -751,6 +796,7 @@ <h3>Notes</h3>
if (!window.showOpenFilePicker) {
document.getElementById("open-btn").disabled = true;
document.getElementById("new-btn").disabled = true;
document.getElementById("load-css-btn").style.display = "none";
document.getElementById("api-hint").textContent = "⚠ Requires Chrome or Edge 86+ (File System Access API)";
document.getElementById("api-hint").style.color = "var(--red)";
}
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/board.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test.describe("MarkBoard", () => {

test("theme buttons switch themes", async ({ page }) => {
const themeButtons = page.locator(".theme-btn");
await expect(themeButtons).toHaveCount(4);
await expect(themeButtons).toHaveCount(6); // 4 built-in + Custom (hidden) + + CSS

await page.locator('.theme-btn[data-theme="light"]').click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
Expand Down
Loading