docs: fix all internal cross-references after restructure
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,309 @@
|
||||
# GPX Manager Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a protected admin page at `/gpx-manager` that lists all trip GPX files and supports upload and deletion via the Grav API.
|
||||
|
||||
**Architecture:** A Grav page (`user/pages/03.gpx-manager/`) with a custom Twig template. Access is enforced by the Login plugin via `access.admin.login: true` in page frontmatter. The template renders a section per trip using the Grav page tree, then vanilla JavaScript calls the existing Grav API (`/api/v1/pages{route}/media`) using the browser's live session cookie — no JWT or separate login needed.
|
||||
|
||||
**Tech Stack:** Grav 2.0 Twig, Vanilla JS (fetch API), Grav API plugin v1, Grav Login plugin (page access control)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Grav 2.0.0-rc.9 + Admin2 v2.0.0-rc.15; theme `intotheeast` at `user/themes/intotheeast/`
|
||||
- API base URL: `/api/v1` (`route: /api`, `version_prefix: v1` in `user/plugins/api/api.yaml`)
|
||||
- Session auth: all fetch calls use `credentials: 'include'` — no JWT handling (`session_enabled: true` in api.yaml)
|
||||
- API media routes (confirmed from `user/plugins/api/classes/Api/ApiRouter.php:333`):
|
||||
- `GET /api/v1/pages{route}/media` — list; response `{ data: [{ filename, size, modified, type }] }`
|
||||
- `POST /api/v1/pages{route}/media` — multipart file upload
|
||||
- `DELETE /api/v1/pages{route}/media/{filename}` — delete single file
|
||||
- `{route}` is the full Grav route including leading slash, e.g. `/trips/italy-2025`
|
||||
- Style: teal `#1F6B5A`, warm border `#e0ddd6`, font-family `'DM Sans', sans-serif` — match existing theme tokens
|
||||
- No new plugins, no npm, no build step. All changes inside `user/` only.
|
||||
- The page must be `visible: false` — must not appear in site navigation.
|
||||
- Trip pages live at `user/pages/01.trips/<slug>/`; retrieved via `grav.pages.find('/trips').children.published()`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Page definition
|
||||
|
||||
**Files:**
|
||||
- Create: `user/pages/03.gpx-manager/gpx-manager.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: Grav page routed at `/gpx-manager`, protected by Login plugin, hidden from nav, using template `gpx-manager`
|
||||
|
||||
- [ ] **Step 1: Create the page file**
|
||||
|
||||
Create `user/pages/03.gpx-manager/gpx-manager.md` with this exact content:
|
||||
|
||||
```
|
||||
---
|
||||
title: 'GPX Manager'
|
||||
template: gpx-manager
|
||||
visible: false
|
||||
routable: true
|
||||
access:
|
||||
admin.login: true
|
||||
---
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify protection (no template yet)**
|
||||
|
||||
With the dev server running, open `http://localhost:8081/gpx-manager` while **logged out** of admin. You should be redirected to the login page. While **logged in**, you'll see a blank page or a Twig error (template missing) — that's fine at this stage.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add pages/03.gpx-manager/gpx-manager.md
|
||||
git -C user commit -m "feat: add gpx-manager page definition (access-protected)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Template — layout and trip sections
|
||||
|
||||
**Files:**
|
||||
- Create: `user/themes/intotheeast/templates/gpx-manager.html.twig`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `grav.pages.find('/trips').children.published()` — each trip object exposes `.route` (string, e.g. `/trips/italy-2025`), `.title` (string), `.slug` (string, e.g. `italy-2025`)
|
||||
- Produces: one `.gpx-trip[data-route]` section per trip; `data-route` = full route string (e.g. `/trips/italy-2025`); `data-trip-route` on upload form = same value
|
||||
|
||||
- [ ] **Step 1: Create the template**
|
||||
|
||||
Create `user/themes/intotheeast/templates/gpx-manager.html.twig`:
|
||||
|
||||
```twig
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% set trips_page = grav.pages.find('/trips') %}
|
||||
{% set trips = trips_page ? trips_page.children.published() : [] %}
|
||||
|
||||
<div class="gpx-manager">
|
||||
<h1 class="gpx-manager__title">GPX Files</h1>
|
||||
|
||||
{% if trips is empty %}
|
||||
<p>No trips found.</p>
|
||||
{% else %}
|
||||
{% for trip in trips %}
|
||||
<section class="gpx-trip" data-route="{{ trip.route }}">
|
||||
<h2 class="gpx-trip__name">{{ trip.title }}</h2>
|
||||
<div class="gpx-file-list" id="files-{{ trip.slug }}">
|
||||
<p class="gpx-loading">Loading…</p>
|
||||
</div>
|
||||
<form class="gpx-upload-form" data-trip-route="{{ trip.route }}">
|
||||
<label class="gpx-upload-label">
|
||||
<input type="file" accept=".gpx,application/gpx+xml" name="file" class="gpx-file-input">
|
||||
</label>
|
||||
<button type="submit" class="gpx-upload-btn">Upload</button>
|
||||
<span class="gpx-status"></span>
|
||||
</form>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.gpx-manager { max-width: 720px; margin: 2rem auto; padding: 0 1rem; font-family: 'DM Sans', sans-serif; }
|
||||
.gpx-manager__title { font-family: 'DM Serif Display', serif; font-size: 1.75rem; margin-bottom: 2rem; }
|
||||
.gpx-trip { border: 1px solid #e0ddd6; border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
|
||||
.gpx-trip__name { font-size: 1.1rem; font-weight: 600; margin: 0 0 1rem; }
|
||||
.gpx-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||
.gpx-table th { text-align: left; color: #666; font-weight: 500; padding: 0.25rem 0.5rem; border-bottom: 1px solid #e0ddd6; }
|
||||
.gpx-table td { padding: 0.5rem; border-bottom: 1px solid #f0ede8; }
|
||||
.gpx-empty, .gpx-loading { color: #888; font-size: 0.875rem; margin-bottom: 0.75rem; }
|
||||
.gpx-upload-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
||||
.gpx-upload-btn { background: #1F6B5A; color: #fff; border: none; border-radius: 5px; padding: 0.4rem 1rem; font-size: 0.875rem; cursor: pointer; }
|
||||
.gpx-upload-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.gpx-delete { background: none; border: 1px solid #ccc; border-radius: 4px; padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer; color: #c0392b; }
|
||||
.gpx-delete:disabled { opacity: 0.5; }
|
||||
.gpx-status { font-size: 0.8rem; color: #555; }
|
||||
.gpx-status.error { color: #c0392b; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* GPX manager JS — added in Task 3 */
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify trip sections render**
|
||||
|
||||
Open `http://localhost:8081/gpx-manager` while logged in. You should see:
|
||||
- Heading "GPX Files"
|
||||
- One card per trip (Italy 2025, Japan-Korea 2026) each showing "Loading…" and an upload form with a file picker and Upload button.
|
||||
- The page header/nav from `base.html.twig` is present.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/gpx-manager.html.twig
|
||||
git -C user commit -m "feat: gpx-manager template layout with trip sections"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: JavaScript — list, upload, delete
|
||||
|
||||
**Files:**
|
||||
- Modify: `user/themes/intotheeast/templates/gpx-manager.html.twig` — replace `/* GPX manager JS — added in Task 3 */` inside the existing `<script>` tag
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `.gpx-trip[data-route]` and `.gpx-upload-form[data-trip-route]` from Task 2
|
||||
- Consumes: Grav API at `/api/v1` (session cookie auth)
|
||||
- API list response: `{ data: [{ filename: string, size: number, modified: string, type: string }] }`
|
||||
- API upload: multipart `FormData` with field name `file`
|
||||
- API delete: `DELETE /api/v1/pages{route}/media/{encodedFilename}` → 200 or 204 on success
|
||||
|
||||
- [ ] **Step 1: Replace the placeholder comment with the full script**
|
||||
|
||||
In `user/themes/intotheeast/templates/gpx-manager.html.twig`, replace `/* GPX manager JS — added in Task 3 */` with:
|
||||
|
||||
```javascript
|
||||
const API = '/api/v1';
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1024).toFixed(0) + ' KB';
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
async function apiFetch(url, options) {
|
||||
const res = await fetch(url, { credentials: 'include', ...options });
|
||||
if (res.status === 401) { window.location.href = '/admin'; return null; }
|
||||
return res;
|
||||
}
|
||||
|
||||
async function loadFiles(tripRoute) {
|
||||
const res = await apiFetch(`${API}/pages${tripRoute}/media`);
|
||||
if (!res || !res.ok) return [];
|
||||
const data = await res.json();
|
||||
return (data.data || []).filter(f => f.filename.toLowerCase().endsWith('.gpx'));
|
||||
}
|
||||
|
||||
async function renderTrip(tripEl) {
|
||||
const route = tripEl.dataset.route;
|
||||
const list = tripEl.querySelector('.gpx-file-list');
|
||||
list.innerHTML = '<p class="gpx-loading">Loading…</p>';
|
||||
|
||||
const files = await loadFiles(route);
|
||||
|
||||
if (files.length === 0) {
|
||||
list.innerHTML = '<p class="gpx-empty">No GPX files.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = files.map(f =>
|
||||
`<tr>
|
||||
<td>${f.filename}</td>
|
||||
<td>${formatSize(f.size)}</td>
|
||||
<td>${formatDate(f.modified)}</td>
|
||||
<td><button class="gpx-delete" data-filename="${f.filename}">Delete</button></td>
|
||||
</tr>`
|
||||
).join('');
|
||||
|
||||
list.innerHTML = `<table class="gpx-table">
|
||||
<thead><tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
|
||||
list.querySelectorAll('.gpx-delete').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm(`Delete ${btn.dataset.filename}?`)) return;
|
||||
btn.disabled = true;
|
||||
const res = await apiFetch(
|
||||
`${API}/pages${route}/media/${encodeURIComponent(btn.dataset.filename)}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
if (res && (res.ok || res.status === 204)) {
|
||||
await renderTrip(tripEl);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
alert('Delete failed — check console.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initUpload(formEl) {
|
||||
formEl.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const route = formEl.dataset.tripRoute;
|
||||
const fileInput = formEl.querySelector('input[type=file]');
|
||||
const file = fileInput.files[0];
|
||||
const status = formEl.querySelector('.gpx-status');
|
||||
const btn = formEl.querySelector('.gpx-upload-btn');
|
||||
|
||||
if (!file) { status.textContent = 'Choose a file first.'; return; }
|
||||
|
||||
status.textContent = 'Uploading…';
|
||||
status.className = 'gpx-status';
|
||||
btn.disabled = true;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
|
||||
const res = await apiFetch(`${API}/pages${route}/media`, { method: 'POST', body: fd });
|
||||
btn.disabled = false;
|
||||
|
||||
if (res && res.ok) {
|
||||
status.textContent = 'Uploaded!';
|
||||
fileInput.value = '';
|
||||
await renderTrip(formEl.closest('.gpx-trip'));
|
||||
setTimeout(() => { status.textContent = ''; }, 3000);
|
||||
} else {
|
||||
const err = res ? await res.json().catch(() => ({})) : {};
|
||||
status.textContent = 'Error: ' + (err.detail || (res ? res.statusText : 'network error'));
|
||||
status.className = 'gpx-status error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.gpx-trip').forEach(renderTrip);
|
||||
document.querySelectorAll('.gpx-upload-form').forEach(initUpload);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test file listing**
|
||||
|
||||
Open `http://localhost:8081/gpx-manager` while logged in. Open DevTools → Network tab.
|
||||
|
||||
Expected:
|
||||
- `GET /api/v1/pages/trips/italy-2025/media` → 200, Italy 2025 section shows a table with 3 rows (day-5, day-6, day-8) with sizes (~1.8 MB, ~2.2 MB, ~1.9 MB) and dates.
|
||||
- `GET /api/v1/pages/trips/japan-korea-2026/media` → 200, Japan-Korea 2026 section shows "No GPX files."
|
||||
|
||||
- [ ] **Step 3: Test upload**
|
||||
|
||||
In the Japan-Korea 2026 section: click the file input, select any `.gpx` file from disk, click Upload.
|
||||
|
||||
Expected:
|
||||
- Status shows "Uploading…" then "Uploaded!"
|
||||
- The file table re-renders with the new file listed.
|
||||
- DevTools shows `POST /api/v1/pages/trips/japan-korea-2026/media` → 200.
|
||||
|
||||
- [ ] **Step 4: Test delete**
|
||||
|
||||
Click Delete on the file just uploaded. Confirm the dialog.
|
||||
|
||||
Expected:
|
||||
- The row disappears immediately.
|
||||
- DevTools shows `DELETE /api/v1/pages/trips/japan-korea-2026/media/<filename>` → 200 or 204.
|
||||
- Reload the page — file is gone.
|
||||
|
||||
- [ ] **Step 5: Test 401 redirect**
|
||||
|
||||
Log out of Admin2. In a new tab, navigate to `http://localhost:8081/gpx-manager`.
|
||||
|
||||
Expected: redirected to login page (Login plugin enforces `access.admin.login: true` before the page renders, so the JS never runs).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git -C user add themes/intotheeast/templates/gpx-manager.html.twig
|
||||
git -C user commit -m "feat: gpx-manager list, upload, delete via Grav API session auth"
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# Stats Redesign — Implementation Plan
|
||||
|
||||
*Derived from spec: docs/superpowers/specs/2026-06-19-stats-redesign.md*
|
||||
*Derived from spec: docs/working/specs/2026-06-19-stats-redesign.md*
|
||||
|
||||
> **For agentic workers:** Use superpowers:subagent-driven-development to execute this plan task-by-task.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Status:** 🔄 In progress — Task 1 complete (skip link), Tasks 2–6 open
|
||||
|
||||
**Goal:** Fix all eight WCAG 2.1 AA failures identified in the accessibility audit and add axe-core Playwright regression tests.
|
||||
|
||||
**Architecture:** Six sequential tasks — each implements one audit finding (or related group), writes a Playwright test first, then implements the fix in the relevant template/CSS/JS files. All tests go into a new `tests/ui/accessibility.spec.js` file that grows task by task. Task 6 adds axe-core automated scans on top of the feature-specific checks.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## Global Constraints
|
||||
|
||||
- All file moves use `git mv` — never `mv` — so git history is preserved.
|
||||
- The existing spec file (`docs/superpowers/specs/2026-06-21-documentation-restructure-design.md`) is itself one of the files being moved — move it in Task 1 with the rest.
|
||||
- The existing spec file (`docs/working/specs/2026-06-21-documentation-restructure-design.md`) is itself one of the files being moved — move it in Task 1 with the rest.
|
||||
- Do NOT modify any file inside `user/` — that is a separate git repo.
|
||||
- Do NOT touch memory files outside of Task 9.
|
||||
- CLAUDE.md lives at repo root and stays there.
|
||||
@@ -43,12 +43,12 @@ mkdir -p /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs/research
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
|
||||
# All specs
|
||||
for f in docs/superpowers/specs/*.md; do
|
||||
for f in docs/working/specs/*.md; do
|
||||
git mv "$f" "docs/working/specs/$(basename "$f")"
|
||||
done
|
||||
|
||||
# All plans
|
||||
for f in docs/superpowers/plans/*.md; do
|
||||
for f in docs/working/plans/*.md; do
|
||||
git mv "$f" "docs/working/plans/$(basename "$f")"
|
||||
done
|
||||
```
|
||||
@@ -57,10 +57,10 @@ done
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
git mv docs/milestone-1-spec.md docs/working/milestones/milestone-1.md
|
||||
git mv docs/milestone-2-spec.md docs/working/milestones/milestone-2.md
|
||||
git mv docs/milestone-3-spec.md docs/working/milestones/milestone-3.md
|
||||
git mv docs/milestone-4-spec.md docs/working/milestones/milestone-4.md
|
||||
git mv docs/working/milestones/milestone-1.md docs/working/milestones/milestone-1.md
|
||||
git mv docs/working/milestones/milestone-2.md docs/working/milestones/milestone-2.md
|
||||
git mv docs/working/milestones/milestone-3.md docs/working/milestones/milestone-3.md
|
||||
git mv docs/working/milestones/milestone-4.md docs/working/milestones/milestone-4.md
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Move QA docs**
|
||||
@@ -103,10 +103,10 @@ rmdir docs/design # now empty
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
git mv docs/posting-pipeline.md docs/guides/posting.md
|
||||
git mv docs/guides/posting.md docs/guides/posting.md
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Verify — no files remain at docs/ root, no docs/superpowers/ exists**
|
||||
- [ ] **Step 9: Verify — no files remain at docs/ root, no docs/working/ exists**
|
||||
|
||||
```bash
|
||||
find /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs -maxdepth 1 -type f
|
||||
@@ -137,27 +137,27 @@ git commit -m "docs: restructure docs/ into guides/ reference/ working/ research
|
||||
**Files:**
|
||||
- Modify: all files under `docs/working/` and `docs/research/` that reference old paths
|
||||
|
||||
After the moves, links inside plan and spec files still point to old paths like `docs/superpowers/plans/...` and `docs/milestone-1-spec.md`. This task fixes them all.
|
||||
After the moves, links inside plan and spec files still point to old paths like `docs/working/plans/...` and `docs/working/milestones/milestone-1.md`. This task fixes them all.
|
||||
|
||||
- [ ] **Step 1: Replace docs/superpowers/specs/ references**
|
||||
- [ ] **Step 1: Replace docs/working/specs/ references**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
grep -rl "docs/superpowers/specs/" docs/ | xargs sed -i 's|docs/superpowers/specs/|docs/working/specs/|g'
|
||||
grep -rl "docs/working/specs/" docs/ | xargs sed -i 's|docs/working/specs/|docs/working/specs/|g'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace docs/superpowers/plans/ references**
|
||||
- [ ] **Step 2: Replace docs/working/plans/ references**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
grep -rl "docs/superpowers/plans/" docs/ | xargs sed -i 's|docs/superpowers/plans/|docs/working/plans/|g'
|
||||
grep -rl "docs/working/plans/" docs/ | xargs sed -i 's|docs/working/plans/|docs/working/plans/|g'
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace docs/superpowers/ catch-all (any remaining bare references)**
|
||||
- [ ] **Step 3: Replace docs/working/ catch-all (any remaining bare references)**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
grep -rl "docs/superpowers/" docs/ | xargs sed -i 's|docs/superpowers/|docs/working/|g'
|
||||
grep -rl "docs/working/" docs/ | xargs sed -i 's|docs/working/|docs/working/|g'
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace milestone spec references**
|
||||
@@ -181,16 +181,16 @@ grep -rl "docs/posting-pipeline" docs/ | xargs sed -i 's|docs/posting-pipeline\.
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
sed -i 's|docs/superpowers/specs/|docs/working/specs/|g' CLAUDE.md
|
||||
sed -i 's|docs/superpowers/plans/|docs/working/plans/|g' CLAUDE.md
|
||||
sed -i 's|docs/superpowers/|docs/working/|g' CLAUDE.md
|
||||
sed -i 's|docs/working/specs/|docs/working/specs/|g' CLAUDE.md
|
||||
sed -i 's|docs/working/plans/|docs/working/plans/|g' CLAUDE.md
|
||||
sed -i 's|docs/working/|docs/working/|g' CLAUDE.md
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify no old paths remain**
|
||||
|
||||
```bash
|
||||
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
|
||||
grep -r "docs/superpowers/" docs/ CLAUDE.md
|
||||
grep -r "docs/working/" docs/ CLAUDE.md
|
||||
# Expected: no output
|
||||
grep -r "docs/milestone-[0-9]-spec" docs/ CLAUDE.md
|
||||
# Expected: no output
|
||||
@@ -794,7 +794,7 @@ Full setup guide: [`docs/guides/local-setup.md`](docs/guides/local-setup.md)
|
||||
Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
||||
|
||||
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default.
|
||||
The brainstorming and writing-plans skills default to `docs/working/`; these lines override that default.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify CLAUDE.md**
|
||||
@@ -1016,7 +1016,7 @@ git commit -m "docs: add architecture overview reference"
|
||||
**Files:**
|
||||
- Modify: 4 memory files in `~/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/`
|
||||
|
||||
Four memory files still reference `docs/superpowers/` paths. Update them to `docs/working/`.
|
||||
Four memory files still reference `docs/working/` paths. Update them to `docs/working/`.
|
||||
|
||||
- [ ] **Step 1: Identify affected lines**
|
||||
|
||||
@@ -1032,9 +1032,9 @@ grep -n "docs/superpowers" "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-ex
|
||||
MEMORY_DIR="/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory"
|
||||
for f in "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-execution-gap.md" \
|
||||
"$MEMORY_DIR/project-story-mode-and-maplibre.md" "$MEMORY_DIR/project-homepage-redesign.md"; do
|
||||
sed -i 's|docs/superpowers/specs/|docs/working/specs/|g' "$f"
|
||||
sed -i 's|docs/superpowers/plans/|docs/working/plans/|g' "$f"
|
||||
sed -i 's|docs/superpowers/|docs/working/|g' "$f"
|
||||
sed -i 's|docs/working/specs/|docs/working/specs/|g' "$f"
|
||||
sed -i 's|docs/working/plans/|docs/working/plans/|g' "$f"
|
||||
sed -i 's|docs/working/|docs/working/|g' "$f"
|
||||
done
|
||||
```
|
||||
|
||||
@@ -1055,7 +1055,7 @@ Verify MEMORY.md now reads correctly:
|
||||
```bash
|
||||
grep "plans\|specs\|superpowers\|working" \
|
||||
"/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/MEMORY.md"
|
||||
# Expected: all plan/spec references point to docs/working/, none to docs/superpowers/
|
||||
# Expected: all plan/spec references point to docs/working/, none to docs/working/
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Final verification — complete restructure**
|
||||
|
||||
@@ -29,7 +29,7 @@ The existing 4-stat grid expands to 6 stats. Both `stats.html.twig` and the inli
|
||||
|
||||
| Stat | Label | Source | Notes |
|
||||
|---|---|---|---|
|
||||
| Days on the road | `days on the road` | `(now - first entry date) / 86400` | Unchanged |
|
||||
| Days on the road | `days on the road` | `date_end - date_start` if trip `date_end` is set; else `now - first entry date` | Fixed for past trips |
|
||||
| Entries posted | `entries posted` | `all_entries\|length` | Unchanged |
|
||||
| Countries visited | `countries visited` | Deduplicated `location_country` | Unchanged; country list shown below grid |
|
||||
| **Cities visited** | `cities visited` | Deduplicated `location_city` | New; same dedup logic as countries |
|
||||
@@ -82,19 +82,10 @@ Max speed is explicitly excluded — GPS noise at 1-second resolution produces u
|
||||
|
||||
### Icon system
|
||||
|
||||
The GPX `<type>` tag on the track element drives the icon shown in both the main stats distance block and the cycling panel header:
|
||||
A single static racing/gravel bike icon is used whenever GPX files are present — both in the main stats distance block and the cycling panel header. No dynamic switching based on `<type>`.
|
||||
|
||||
| `<type>` value | Icon |
|
||||
|---|---|
|
||||
| `racebike` | Road bike |
|
||||
| `touringbicycle` | Touring bike |
|
||||
| `mtb` | Mountain bike |
|
||||
| `cycling` (generic) | Generic bike |
|
||||
| `hiking` | Hiking boot |
|
||||
| `hike` | Hiking boot |
|
||||
| Any unrecognised value | Generic bike (fallback) |
|
||||
|
||||
When multiple GPX files exist with different types, use the type from the first file. This is an acceptable heuristic for now.
|
||||
Known Komoot `<type>` values for reference (future use if icon switching is ever added):
|
||||
`racebike`, `touringbicycle`, `mtb`, `cycling`, `hiking`, `hike`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
# Design Spec: Story Mode + MapLibre Migration
|
||||
|
||||
*Date: 2026-06-19*
|
||||
*Inspired by: [Sabdia](https://github.com/m-cluitmans/Sabdia) — a friend's sabbatical blog built on Astro + Keystatic + MapLibre*
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Two parallel features:
|
||||
|
||||
1. **Story Mode** — a rich long-form post type alongside journal dailies, with cinematic
|
||||
storytelling blocks (hero, chapter breaks, scrollytelling, pull quotes, snap gallery)
|
||||
2. **MapLibre GL migration** — replace Leaflet across all three maps (full map, mini-map,
|
||||
home map) with MapLibre GL JS; add animated journey line; improve CSS integration
|
||||
|
||||
---
|
||||
|
||||
## Decisions Log
|
||||
|
||||
### Why MapLibre GL instead of Leaflet
|
||||
|
||||
Leaflet renders raster PNG tiles. MapLibre GL renders vector tiles in WebGL. Key gains:
|
||||
|
||||
- **Animated journey line** — MapLibre's GeoJSON source model makes RAF-loop animation
|
||||
trivial (`source.setData()` per frame). On Leaflet you'd call `polyline.setLatLngs()`
|
||||
which also works, but MapLibre gives us everything below for free too.
|
||||
- **Smooth zoom** — continuous sub-pixel zoom vs Leaflet's tile-snap zoom levels
|
||||
- **Retina crisp** — vector geometry scales perfectly on HiDPI screens
|
||||
- **Future-proof** — 3D terrain, tilt/pitch, per-feature click events, style control,
|
||||
outdoor/topo/satellite styles for GPX track maps all become straightforward
|
||||
- **GPX styling** — switching from `leaflet-gpx` to `@mapbox/togeojson` + GeoJSON layer
|
||||
gives per-point colour control (speed, elevation gradients) later
|
||||
|
||||
Cost: ~280KB (vs ~40KB Leaflet). Acceptable — cached after first visit.
|
||||
|
||||
Tile source stays the same: CARTO dark vector style — free, no API key.
|
||||
|
||||
### Why shortcodes for story blocks (not modular pages or blueprint lists)
|
||||
|
||||
Evaluated three approaches for in-prose storytelling blocks:
|
||||
|
||||
| Approach | What it is | Verdict |
|
||||
|---|---|---|
|
||||
| **Shortcodes** | `[chapter-break ...]` inline in Markdown | ✅ Chosen |
|
||||
| Modular pages | Each block = a child page in Admin | ✗ Ruled out |
|
||||
| Blueprint list + elements | `sections:` YAML list with type selector | ✗ Ruled out |
|
||||
|
||||
**Modular pages** are how most Grav storytelling themes work (Quark, Oxygen, all
|
||||
HTML5UP ports). Each block gets proper Admin form fields. But a 1,500-word story with
|
||||
two chapter breaks requires five child pages — navigating between them on mobile while
|
||||
traveling is painful. Prose ends up fragmented across "text" module pages.
|
||||
|
||||
**Blueprint list with elements field** (Grav's conditional field groups) could render
|
||||
blocks as a structured "Add section" list in Admin. But prose still has to go in a
|
||||
"text" type section, so a story becomes a long list of `text/chapter-break/text/scrolly/
|
||||
text/gallery` entries rather than a flowing document.
|
||||
|
||||
**Shortcodes** keep everything in one Markdown editor — prose flows naturally, blocks are
|
||||
inserted inline. The `shortcode-gallery-plusplus` plugin already in our stack brings
|
||||
`shortcode-core` as a dependency, so no new plugin is needed.
|
||||
|
||||
Grav Admin2 has no rich block-editor like Keystatic/Markdoc. Shortcodes are the
|
||||
closest practical equivalent for mixed prose+blocks authoring on mobile.
|
||||
|
||||
*Future option:* If Admin2 ever gains inline block components (or we add a Flex Object
|
||||
definition), the shortcode content can be migrated — the block semantics are identical.
|
||||
|
||||
### Why gallery stays as lightbox on journal entries
|
||||
|
||||
Journal entries are short daily posts — a grid of 3–8 photos suits them.
|
||||
The snap gallery is a deliberate slow storytelling device (one photo fills the screen,
|
||||
reader swipes through). That pacing fits stories, not a daily feed card.
|
||||
|
||||
### Weather not added to story frontmatter
|
||||
|
||||
Weather is a journal-entry concept (captured at the moment of a daily post via
|
||||
Open-Meteo). Stories are retrospective long-form narratives — weather would be referenced
|
||||
in prose if relevant, not as a metadata badge.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — Story Mode
|
||||
|
||||
### 1.1 Page structure
|
||||
|
||||
Stories live as child pages under `04.stories/`:
|
||||
|
||||
```
|
||||
user/pages/01.trips/<trip-slug>/04.stories/
|
||||
stories.md ← listing page, template: stories
|
||||
01.<story-slug>/
|
||||
story.md ← individual story, template: story
|
||||
hero.jpg
|
||||
photo-a.jpg
|
||||
photo-b.jpg
|
||||
```
|
||||
|
||||
`stories.md` frontmatter:
|
||||
```yaml
|
||||
title: Stories
|
||||
template: stories
|
||||
published: true
|
||||
```
|
||||
|
||||
### 1.2 Story frontmatter schema
|
||||
|
||||
```yaml
|
||||
title: Into the Hills of Kyoto
|
||||
date: 2026-03-28 # start date — shown in hero header
|
||||
end_date: 2026-03-29 # optional; shown as "28–29 Mar 2026"
|
||||
location_name: Kyoto # city/region; shown in hero header
|
||||
location_country: Japan # used for stats de-duplication
|
||||
lat: 34.967 # main GPS coordinate — shows pin on /map
|
||||
lng: 135.773
|
||||
hero_image: hero.jpg # filename in page media; required for hero section
|
||||
hero_alt: The vermillion gate at Fushimi Inari at dawn
|
||||
published: true
|
||||
```
|
||||
|
||||
Fields deliberately excluded: `weather_*` (not meaningful for stories).
|
||||
|
||||
### 1.3 Shortcode blocks
|
||||
|
||||
Four blocks implemented as ShortcodeCore shortcodes.
|
||||
All image paths are **filenames only** (e.g. `shrine.jpg`) — resolved against the story's
|
||||
own page media folder, same convention as `hero_image`.
|
||||
|
||||
#### ChapterBreak
|
||||
|
||||
Full-bleed atmospheric photo with a frosted-glass title panel. Reveals on scroll via
|
||||
IntersectionObserver (blur + translateY → clear).
|
||||
|
||||
```
|
||||
[chapter-break image="shrine-gate.jpg" title="The Long Walk Up" number="II" /]
|
||||
```
|
||||
|
||||
| Attribute | Required | Description |
|
||||
|---|---|---|
|
||||
| `image` | yes | Page media filename |
|
||||
| `title` | yes | Chapter title, displayed in frosted panel |
|
||||
| `number` | no | Roman numeral or label shown above title |
|
||||
| `alt` | no | Alt text (defaults to `title`) |
|
||||
|
||||
Renders as `60vh` full-bleed block with dark gradient tint over the image and a
|
||||
`backdrop-filter: blur(18px)` panel containing the chapter number + title + teal rule.
|
||||
|
||||
#### ScrollySection
|
||||
|
||||
NYT-style sticky image (55% left column) with text panels that scroll past on the right.
|
||||
Steps are separated by `---` inside the shortcode body. Powered by **Scrollama** (CDN).
|
||||
|
||||
```
|
||||
[scrolly-section image="torii-path.jpg" alt="Thousands of torii gates"]
|
||||
The path stretched further than I could see.
|
||||
|
||||
---
|
||||
|
||||
Each gate was donated by a business or family, a prayer made physical.
|
||||
|
||||
---
|
||||
|
||||
By the tenth minute of walking, the city had disappeared entirely.
|
||||
[/scrolly-section]
|
||||
```
|
||||
|
||||
| Attribute | Required | Description |
|
||||
|---|---|---|
|
||||
| `image` | yes | Page media filename — sticky background |
|
||||
| `alt` | no | Image alt text |
|
||||
| `caption` | no | Small caption shown bottom-left of image |
|
||||
|
||||
On mobile: full-screen sticky image with text panels scrolling over it (same layout,
|
||||
single column — image behind, text on top with semi-transparent card).
|
||||
|
||||
Image starts blurred (`blur(8px) scale(1.04)`), unblurs when section enters viewport.
|
||||
Between steps: subtle pan (object-position cycles through 5 offsets) + slight overlay
|
||||
darkening for depth.
|
||||
|
||||
#### PullQuote
|
||||
|
||||
Frosted-glass quote block with optional atmospheric background image. Reveals on scroll.
|
||||
|
||||
```
|
||||
[pull-quote image="lanterns.jpg"]
|
||||
The torii gates never seemed to end — and I didn't want them to.
|
||||
[/pull-quote]
|
||||
```
|
||||
|
||||
| Attribute | Required | Description |
|
||||
|---|---|---|
|
||||
| `image` | no | Page media filename — background photo |
|
||||
| `alt` | no | Alt text for background image |
|
||||
|
||||
Without `image`: renders on `--color-canvas` (warm dark surface, solid).
|
||||
With `image`: full-bleed image behind frosted glass panel.
|
||||
|
||||
Large decorative `"` marks above and below the quote text (DM Serif Display, 5rem).
|
||||
|
||||
#### SnapGallery
|
||||
|
||||
Full-screen snap-scroll photo sequence. One photo per swipe. Snap physics are pure CSS
|
||||
(`scroll-snap-type: y mandatory` + `scroll-snap-stop: always` on the scroll container).
|
||||
Dot indicator active state updated via a small IntersectionObserver on each slide.
|
||||
|
||||
```
|
||||
[snap-gallery images="photo-a.jpg,photo-b.jpg,photo-c.jpg" captions="The approach,Summit view,Descent" alts="Hikers on trail,Mountain panorama,Forest path" /]
|
||||
```
|
||||
|
||||
| Attribute | Required | Description |
|
||||
|---|---|---|
|
||||
| `images` | yes | Comma-separated page media filenames |
|
||||
| `captions` | no | Comma-separated captions (positional) |
|
||||
| `alts` | no | Comma-separated alt texts (positional) |
|
||||
|
||||
Each slide: blurred cover-crop background + contain-fit foreground image + caption fades
|
||||
in at bottom. Dot indicator on the right edge. Page-level `scroll-snap-align: start`
|
||||
with `proximity` (not mandatory) so normal page scroll is unaffected.
|
||||
|
||||
### 1.4 Template: `story.html.twig`
|
||||
|
||||
Extends `partials/base.html.twig` but overrides the nav block to show only a floating
|
||||
escape link. Full layout:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ ← Back (position: fixed, top-left) │
|
||||
│ │
|
||||
│ HERO — 100vh │
|
||||
│ sticky image, Ken Burns zoom-out │
|
||||
│ title blurs up from bottom │
|
||||
│ date · location beneath title │
|
||||
│ ↓ bounce scroll indicator │
|
||||
│ 40vh spacer (scroll trigger zone) │
|
||||
│ │
|
||||
├────────────────────────────────────────┤
|
||||
│ STORY BODY │
|
||||
│ max-width: 680px, centred │
|
||||
│ font: DM Serif Display (headings) │
|
||||
│ DM Sans (prose) │
|
||||
│ {{ page.content|raw }} │
|
||||
│ (Markdown + shortcode blocks) │
|
||||
│ │
|
||||
│ ← Back to stories (footer) │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Hero scroll behaviour (vanilla JS, no library):**
|
||||
|
||||
- `window.scroll` listener (passive, rAF-throttled)
|
||||
- `progress = scrollY / innerHeight` (0→1 as hero scrolls away)
|
||||
- At progress > 0: dark overlay fades in (`rgba(0,0,0, progress * 0.6)`)
|
||||
- Scroll indicator hides after `scrollY > 80px`
|
||||
- At progress ≥ 1: overlay removed from DOM
|
||||
|
||||
**Ken Burns animation:** CSS `@keyframes` — `scale(1.06) → scale(1)` over 12s,
|
||||
`ease-out`, `forwards`. Respects `prefers-reduced-motion: reduce`.
|
||||
|
||||
**Text reveal:** Title and date animate in with `filter: blur(10px) + translateY(22px)
|
||||
→ clear` at 0.2s / 0.55s delay. Respects `prefers-reduced-motion`.
|
||||
|
||||
### 1.5 Template: `stories.html.twig`
|
||||
|
||||
Listing of published stories for the active trip. Grid of story cards:
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ hero thumb │ │ hero thumb │
|
||||
│ │ │ │
|
||||
│ Kyoto Hills │ │ Seoul Rain │
|
||||
│ 28–29 Mar │ │ 1 Apr │
|
||||
│ Kyoto │ │ Seoul │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
2-column grid on desktop, single column on mobile. Each card links to the story.
|
||||
Empty state: "No stories yet — check back soon."
|
||||
|
||||
Stories are also listed as cards in `dailies.html.twig`'s combined feed (already
|
||||
implemented — the template merges journal entries and stories by date).
|
||||
|
||||
### 1.6 JS dependencies
|
||||
|
||||
| Library | How loaded | Size | Purpose |
|
||||
|---|---|---|---|
|
||||
| **Scrollama** | CDN (`jsdelivr`) | ~4KB | ScrollySection step detection |
|
||||
| IntersectionObserver | Native browser API | — | ChapterBreak + PullQuote reveal, SnapGallery dots |
|
||||
|
||||
Scrollama is only loaded on story pages (inline `<script src>` in `story.html.twig`).
|
||||
|
||||
### 1.7 CSS additions (story-specific)
|
||||
|
||||
New CSS block added to `style.css` under a `/* ── Story pages ──` section:
|
||||
|
||||
**Story layout:**
|
||||
- `.story-hero` — `position: relative; height: 100vh; overflow: hidden`
|
||||
- `.story-hero__img` — `position: sticky; top: 0; width: 100%; height: 100vh; object-fit: cover`
|
||||
- `.story-hero__overlay` — `position: fixed; inset: 0; pointer-events: none` (JS-driven opacity)
|
||||
- `.story-hero__content` — `position: absolute; bottom: 18%; text-align: center; color: #fff`
|
||||
- `.story-escape` — `position: fixed; top: 1rem; left: 1rem; z-index: 100; color: var(--color-ink); background: var(--color-canvas); ...`
|
||||
- `.story-body` — `max-width: 680px; margin: 0 auto; padding: var(--space-16) var(--space-6)`
|
||||
- `.story-body p` — `font-family: var(--font-ui); font-size: 1.0625rem; line-height: 1.85; color: var(--color-ink-2)`
|
||||
|
||||
**ChapterBreak:**
|
||||
- `.chapter-break` — full-bleed breakout, `60vh`, overflow hidden
|
||||
- `.chapter-break__panel` — `backdrop-filter: blur(18px); background: rgba(26,24,20,0.25); border: 1px solid rgba(255,255,255,0.12); border-radius: var(--radius-sm)`
|
||||
- Initial state: `opacity: 0; filter: blur(12px); transform: translateY(28px)` → `.is-revealed` clears all
|
||||
- `.chapter-break__rule` — `40px × 2px` teal (`var(--color-accent)`) rule below title
|
||||
|
||||
**ScrollySection:**
|
||||
- `.scrolly` — `display: grid; grid-template-columns: 55% 45%; width: 100vw` (full-bleed breakout)
|
||||
- `.scrolly__media` — `position: sticky; top: var(--site-header-height); height: calc(100vh - var(--site-header-height))`
|
||||
- `.scrolly-step__inner` — `background: rgba(26,24,20,0.92); backdrop-filter: blur(4px); border-radius: var(--radius-sm); border: 1px solid var(--color-border)`
|
||||
- Mobile (`max-width: 768px`): single column, steps overlay the sticky image with `margin-top: calc(-(100vh - var(--site-header-height)))`
|
||||
|
||||
**PullQuote:**
|
||||
- `.pull-quote` — bleeds `1.5rem` each side beyond prose column
|
||||
- `.pull-quote__inner` — `backdrop-filter: blur(14px); background: rgba(26,24,20,0.12)` (with image) or `var(--color-canvas)` (without)
|
||||
- Large `"` marks: `font-family: var(--font-display); font-size: 5rem; color: var(--color-accent); opacity: 0.4`
|
||||
|
||||
**SnapGallery:**
|
||||
- `.pgallery__frame` — `height: 100vh; scroll-snap-type: y mandatory; overflow-y: scroll`
|
||||
- `.pgallery__bg` — `object-fit: cover; filter: blur(20px) brightness(0.4)` (blurred backdrop)
|
||||
- `.pgallery__fg` — `object-fit: contain` (full foreground image)
|
||||
- `.pgallery__dot.is-active` — `background: var(--color-accent)`
|
||||
|
||||
All animations respect `prefers-reduced-motion: reduce` — transitions set to `none`,
|
||||
initial states set to final states immediately.
|
||||
|
||||
### 1.8 Demo story content
|
||||
|
||||
One sample story added to `user/docs/demo/trips/japan-korea-2026/` following existing
|
||||
demo conventions. Story covers 28–29 March (Kyoto days already in journal demo):
|
||||
|
||||
```
|
||||
user/docs/demo/trips/japan-korea-2026/04.stories/01.the-thousand-gates/
|
||||
story.md
|
||||
```
|
||||
|
||||
Frontmatter mirrors the schema. Body uses all four shortcode types so they can be QA'd
|
||||
in one pass. No binary image assets — `make demo-load` copies the folder; tester drops
|
||||
a few JPEGs in to exercise hero + photo blocks.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — MapLibre GL Migration
|
||||
|
||||
### 2.1 Scope
|
||||
|
||||
Three files change. No new page routes. GPX file storage and delivery unchanged.
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `map.html.twig` | Full rewrite of JS + CDN refs; CSS class renames |
|
||||
| `dailies.html.twig` | Mini-map JS + CDN refs rewritten |
|
||||
| `home.html.twig` | Home map JS + CDN refs rewritten |
|
||||
| `style.css` | Leaflet overrides removed; MapLibre overrides added |
|
||||
|
||||
CDN changes (all three map templates):
|
||||
|
||||
```html
|
||||
<!-- Remove -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-gpx@2.1.2/gpx.min.js"></script>
|
||||
|
||||
<!-- Add -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
|
||||
<!-- GPX maps only: -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
|
||||
```
|
||||
|
||||
Tile style URL (same CARTO dark, now as vector style):
|
||||
```
|
||||
https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json
|
||||
```
|
||||
|
||||
### 2.2 Animated journey line
|
||||
|
||||
Port of Sabdia's `animateJourneyLine` to vanilla JS against MapLibre's GeoJSON source API:
|
||||
|
||||
```js
|
||||
map.on('load', () => {
|
||||
// Add an empty source
|
||||
map.addSource('journey', {
|
||||
type: 'geojson',
|
||||
data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } }
|
||||
});
|
||||
|
||||
// Glow layer (wide, low opacity)
|
||||
map.addLayer({ id: 'journey-glow', type: 'line', source: 'journey',
|
||||
paint: { 'line-color': '#2A8C73', 'line-width': 6, 'line-opacity': 0.18 }
|
||||
});
|
||||
|
||||
// Main line
|
||||
map.addLayer({ id: 'journey-line', type: 'line', source: 'journey',
|
||||
paint: { 'line-color': '#2A8C73', 'line-width': 2.5, 'line-opacity': 0.85 }
|
||||
});
|
||||
|
||||
animateJourneyLine(map, coords); // RAF loop, ease-out cubic, 5000ms
|
||||
});
|
||||
```
|
||||
|
||||
RAF loop builds coordinate array incrementally using cumulative Euclidean distance +
|
||||
ease-out cubic easing. On `prefers-reduced-motion: reduce`: skip animation, set full
|
||||
coordinates immediately.
|
||||
|
||||
Teal values use `var(--color-accent)` equivalent (`#2A8C73`) — matches our design tokens.
|
||||
|
||||
### 2.3 GPX rendering
|
||||
|
||||
Replace `leaflet-gpx` with `@mapbox/togeojson` + MapLibre GeoJSON source:
|
||||
|
||||
```js
|
||||
fetch(gpxUrl)
|
||||
.then(r => r.text())
|
||||
.then(text => {
|
||||
const gpx = new DOMParser().parseFromString(text, 'text/xml');
|
||||
const geojson = toGeoJSON.gpx(gpx);
|
||||
map.addSource('gpx-track', { type: 'geojson', data: geojson });
|
||||
map.addLayer({
|
||||
id: 'gpx-track-line', type: 'line', source: 'gpx-track',
|
||||
paint: { 'line-color': '#2A8C73', 'line-width': 2, 'line-opacity': 0.7 }
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Multiple GPX files (trip has several tracks): each gets its own numbered source/layer pair.
|
||||
|
||||
### 2.4 Markers and popups
|
||||
|
||||
MapLibre uses `maplibregl.Marker` (custom DOM element) + `maplibregl.Popup`.
|
||||
Existing popup HTML content (hero thumbnail, date, title, link) is unchanged.
|
||||
|
||||
Marker style (same visual as current):
|
||||
- Regular entries: `12px` teal dot with white border
|
||||
- Latest/current entry: `18px` teal dot with outer ring (`box-shadow: 0 0 0 4px rgba(42,140,115,0.25)`)
|
||||
|
||||
Popup styled via CSS (see §2.5).
|
||||
|
||||
### 2.5 CSS improvements over Leaflet
|
||||
|
||||
**Remove (Leaflet-specific):**
|
||||
```css
|
||||
/* DELETE — no longer needed */
|
||||
.leaflet-container { background: #282828 !important; }
|
||||
```
|
||||
|
||||
MapLibre sets its canvas background from the style JSON (`background-color` in the style's
|
||||
`background` layer). CARTO dark-matter style uses `#1a1a1a` — no flash on load.
|
||||
|
||||
**Add (MapLibre):**
|
||||
```css
|
||||
/* ── MapLibre GL overrides ───────────────────────────────────────────────────── */
|
||||
|
||||
/* Navigation controls (zoom +/−, compass) */
|
||||
.maplibregl-ctrl-group {
|
||||
background: var(--color-canvas);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.maplibregl-ctrl-group button {
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
.maplibregl-ctrl-group button:hover {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.maplibregl-ctrl-group button + button {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Attribution bar */
|
||||
.maplibregl-ctrl-attrib {
|
||||
background: rgba(26,24,20,0.75) !important;
|
||||
color: var(--color-ink-muted) !important;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.7rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.maplibregl-ctrl-attrib a {
|
||||
color: var(--color-accent) !important;
|
||||
}
|
||||
|
||||
/* Popup */
|
||||
.maplibregl-popup-content {
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-ui);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.maplibregl-popup-tip {
|
||||
border-top-color: var(--color-canvas) !important;
|
||||
}
|
||||
.maplibregl-popup-close-button {
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 1.1rem;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
}
|
||||
.maplibregl-popup-close-button:hover {
|
||||
color: var(--color-ink);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Cursor — pointer hand over clickable markers */
|
||||
.maplibregl-canvas-container.maplibregl-interactive {
|
||||
cursor: grab;
|
||||
}
|
||||
.maplibregl-canvas-container.maplibregl-interactive:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
```
|
||||
|
||||
**Mobile scroll-trap prevention:** For embedded maps (mini-map on dailies, home map),
|
||||
initialize with `cooperativeGestures: true` — requires two fingers to pan on touch.
|
||||
The full-page `/map` uses normal gestures (`cooperativeGestures: false`, the default).
|
||||
*Note: verify `cooperativeGestures` is available in the chosen MapLibre GL 4.x version
|
||||
during implementation; if absent, use `dragPan: false` on touch-only + a two-finger
|
||||
hint overlay as fallback.*
|
||||
|
||||
### 2.6 What is NOT migrated now
|
||||
|
||||
Features from Sabdia's map that were explicitly deferred:
|
||||
|
||||
| Feature | Decision |
|
||||
|---|---|
|
||||
| Ghost pins for upcoming/planned stops | Documented; deferred — requires `show_preview` frontmatter field + Twig logic |
|
||||
| Pulsing amber dot for current location | Documented; deferred — requires "current entry" detection logic |
|
||||
| `flyTo()` on marker click | Deferred — nice UX upgrade, implement after migration stabilises |
|
||||
| 3D terrain | Deferred — requires DEM tile source (MapTiler key) |
|
||||
| Per-story inline MapBlock shortcode | Deferred — implement as part of story mode v2 |
|
||||
| MapTiler outdoor/satellite/topo styles for GPX | Deferred — requires MapTiler API key |
|
||||
|
||||
These are preserved here so they can be picked up in a later milestone without needing
|
||||
to re-research the Sabdia implementation.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Story-specific inline MapBlock shortcode (deferred, see §2.6)
|
||||
- Animated hero video (requires server-side FFmpeg, not available in Grav)
|
||||
- Push notifications for new stories
|
||||
- Story-level statistics (word count, reading time)
|
||||
- Co-authoring / Travel Buddy equivalent
|
||||
- 3D flyover video
|
||||
@@ -66,12 +66,12 @@ docs/
|
||||
|
||||
| New path | Current path |
|
||||
|---|---|
|
||||
| `working/specs/*` (13 files) | `docs/superpowers/specs/*` |
|
||||
| `working/plans/*` (14 files) | `docs/superpowers/plans/*` |
|
||||
| `working/milestones/milestone-1.md` | `docs/milestone-1-spec.md` |
|
||||
| `working/milestones/milestone-2.md` | `docs/milestone-2-spec.md` |
|
||||
| `working/milestones/milestone-3.md` | `docs/milestone-3-spec.md` |
|
||||
| `working/milestones/milestone-4.md` | `docs/milestone-4-spec.md` |
|
||||
| `working/specs/*` (13 files) | `docs/working/specs/*` |
|
||||
| `working/plans/*` (14 files) | `docs/working/plans/*` |
|
||||
| `working/milestones/milestone-1.md` | `docs/working/milestones/milestone-1.md` |
|
||||
| `working/milestones/milestone-2.md` | `docs/working/milestones/milestone-2.md` |
|
||||
| `working/milestones/milestone-3.md` | `docs/working/milestones/milestone-3.md` |
|
||||
| `working/milestones/milestone-4.md` | `docs/working/milestones/milestone-4.md` |
|
||||
| `working/backlog.md` | `docs/backlog.md` |
|
||||
| `working/production-todo.md` | `docs/production-todo.md` |
|
||||
| `working/pm-analysis.md` | `docs/pm-analysis.md` |
|
||||
@@ -150,7 +150,7 @@ Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
|
||||
```
|
||||
|
||||
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default so generated files land in the right place automatically.
|
||||
The brainstorming and writing-plans skills default to `docs/working/`; these lines override that default so generated files land in the right place automatically.
|
||||
|
||||
**Keep inline** (always-loaded context Claude needs without following a link):
|
||||
- §0 project specifics (folder layout, stack versions, trip entity architecture, active trip, GPX pipeline, env rules, remote operations, content sync, gitignore)
|
||||
@@ -173,11 +173,11 @@ The brainstorming and writing-plans skills default to `docs/superpowers/`; these
|
||||
|
||||
1. `docs/` contains exactly four subdirectories: `guides/`, `reference/`, `working/`, `research/`
|
||||
2. All 32+ existing files are moved to their new paths; no files remain at `docs/` root except `README.md`
|
||||
3. `docs/superpowers/` no longer exists; content is under `working/`
|
||||
3. `docs/working/` no longer exists; content is under `working/`
|
||||
4. Four new guides exist and cover their stated scope
|
||||
5. `reference/architecture.md` exists and covers stack, plugin roles, template hierarchy, and post data flow
|
||||
6. `docs/README.md` exists with persona-based navigation
|
||||
7. CLAUDE.md no longer contains §2 local setup block; contains pointer to `docs/guides/local-setup.md`
|
||||
8. All internal cross-references in moved files updated to new paths
|
||||
9. Memory files that reference `docs/superpowers/` paths updated to `docs/working/`
|
||||
9. Memory files that reference `docs/working/` paths updated to `docs/working/`
|
||||
10. CLAUDE.md contains superpowers skill path overrides pointing to `docs/working/specs/` and `docs/working/plans/`
|
||||
|
||||
Reference in New Issue
Block a user