136 lines
4.9 KiB
HTML
136 lines
4.9 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
|
||
|
|
{% block title %}Chronicle Timeline — Ontoref{% endblock title %}
|
||
|
|
{% block nav_group_knowledge %}active{% endblock nav_group_knowledge %}
|
||
|
|
{% block main_class %}container mx-auto px-4 py-6{% endblock main_class %}
|
||
|
|
|
||
|
|
{% block head %}
|
||
|
|
<link rel="stylesheet" href="{{ base_url }}/assets/vendor/vis-timeline.min.css" />
|
||
|
|
<style>
|
||
|
|
#timeline-root {
|
||
|
|
width: 100%;
|
||
|
|
height: calc(100vh - 220px);
|
||
|
|
min-height: 400px;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
overflow: hidden;
|
||
|
|
border: 1px solid oklch(var(--bc) / 0.12);
|
||
|
|
}
|
||
|
|
.vis-item { border-radius: 4px; font-size: 12px; }
|
||
|
|
.vis-item.vis-selected { border-color: oklch(var(--p)); background: oklch(var(--p) / 0.15); }
|
||
|
|
.vis-label { font-size: 12px; color: oklch(var(--bc) / 0.85); }
|
||
|
|
.vis-timeline { background: oklch(var(--b1)); color: oklch(var(--bc)); }
|
||
|
|
.vis-time-axis .vis-text { color: oklch(var(--bc) / 0.6); }
|
||
|
|
.vis-panel.vis-center, .vis-panel.vis-left { border-color: oklch(var(--bc) / 0.12); }
|
||
|
|
#detail-panel {
|
||
|
|
position: fixed; right: 1rem; bottom: 1rem; width: 340px;
|
||
|
|
background: oklch(var(--b2)); border: 1px solid oklch(var(--bc) / 0.18);
|
||
|
|
border-radius: 0.75rem; padding: 1rem; z-index: 40;
|
||
|
|
box-shadow: 0 4px 24px oklch(var(--bc) / 0.12);
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
{% endblock head %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="flex items-center justify-between mb-4 gap-4">
|
||
|
|
<h1 class="text-xl font-semibold">Chronicle Timeline</h1>
|
||
|
|
<div class="flex gap-2 items-center">
|
||
|
|
<select id="kind-filter" class="select select-bordered select-sm" onchange="applyFilter()">
|
||
|
|
<option value="">All kinds</option>
|
||
|
|
<option value="adr">ADR</option>
|
||
|
|
<option value="milestone">Milestone</option>
|
||
|
|
<option value="insight">Insight</option>
|
||
|
|
<option value="interaction">Interaction</option>
|
||
|
|
<option value="migration">Migration</option>
|
||
|
|
</select>
|
||
|
|
<button class="btn btn-ghost btn-sm" onclick="timeline.fit()">Fit</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="timeline-root"></div>
|
||
|
|
<div id="detail-panel">
|
||
|
|
<div class="flex justify-between items-start mb-2">
|
||
|
|
<span id="dp-kind" class="badge badge-sm"></span>
|
||
|
|
<button class="btn btn-ghost btn-xs" onclick="document.getElementById('detail-panel').style.display='none'">✕</button>
|
||
|
|
</div>
|
||
|
|
<p id="dp-date" class="text-xs opacity-60 mb-1"></p>
|
||
|
|
<p id="dp-title" class="font-medium mb-1 text-sm"></p>
|
||
|
|
<p id="dp-summary" class="text-xs opacity-75 leading-relaxed"></p>
|
||
|
|
<div id="dp-refs" class="mt-2 flex flex-wrap gap-1"></div>
|
||
|
|
</div>
|
||
|
|
{% endblock content %}
|
||
|
|
|
||
|
|
{% block scripts %}
|
||
|
|
<script src="{{ base_url }}/assets/vendor/vis-timeline.min.js"></script>
|
||
|
|
<script>
|
||
|
|
const DATA = {{ timeline_json | safe }};
|
||
|
|
|
||
|
|
const KIND_COLOR = {
|
||
|
|
adr: { bg: "#3b82f6", fg: "#fff" },
|
||
|
|
milestone: { bg: "#f59e0b", fg: "#000" },
|
||
|
|
insight: { bg: "#10b981", fg: "#fff" },
|
||
|
|
interaction: { bg: "#8b5cf6", fg: "#fff" },
|
||
|
|
migration: { bg: "#ec4899", fg: "#fff" },
|
||
|
|
};
|
||
|
|
function kindColor(k) { return KIND_COLOR[k] || { bg: "#6b7280", fg: "#fff" }; }
|
||
|
|
|
||
|
|
const GROUPS = [...new Set(DATA.map(e => e.kind))].map(k => ({
|
||
|
|
id: k,
|
||
|
|
content: `<span class="text-xs font-mono">${k}</span>`,
|
||
|
|
}));
|
||
|
|
|
||
|
|
let allItems = DATA.map(e => {
|
||
|
|
const c = kindColor(e.kind);
|
||
|
|
return {
|
||
|
|
id: e.id,
|
||
|
|
group: e.kind,
|
||
|
|
start: e.date || "1970-01-01",
|
||
|
|
content: `<span style="max-width:220px;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${e.title}</span>`,
|
||
|
|
title: e.title,
|
||
|
|
style: `background:${c.bg};color:${c.fg};border-color:${c.bg};`,
|
||
|
|
_entry: e,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
const container = document.getElementById('timeline-root');
|
||
|
|
const items = new vis.DataSet(allItems);
|
||
|
|
const groups = new vis.DataSet(GROUPS);
|
||
|
|
|
||
|
|
const timeline = new vis.Timeline(container, items, groups, {
|
||
|
|
stack: true,
|
||
|
|
orientation: { axis: "top" },
|
||
|
|
selectable: true,
|
||
|
|
zoomMin: 1000 * 60 * 60 * 24 * 7,
|
||
|
|
zoomMax: 1000 * 60 * 60 * 24 * 365 * 3,
|
||
|
|
});
|
||
|
|
|
||
|
|
timeline.on('select', ({ items: sel }) => {
|
||
|
|
if (!sel.length) return;
|
||
|
|
const item = allItems.find(i => i.id === sel[0]);
|
||
|
|
if (!item) return;
|
||
|
|
const e = item._entry;
|
||
|
|
const dp = document.getElementById('detail-panel');
|
||
|
|
const c = kindColor(e.kind);
|
||
|
|
document.getElementById('dp-kind').textContent = e.kind;
|
||
|
|
document.getElementById('dp-kind').style.cssText = `background:${c.bg};color:${c.fg}`;
|
||
|
|
document.getElementById('dp-date').textContent = e.date || "";
|
||
|
|
document.getElementById('dp-title').textContent = e.title;
|
||
|
|
document.getElementById('dp-summary').textContent = e.summary || "";
|
||
|
|
const refsEl = document.getElementById('dp-refs');
|
||
|
|
refsEl.innerHTML = (e.refs || []).map(r =>
|
||
|
|
`<span class="badge badge-outline badge-xs">${r}</span>`
|
||
|
|
).join('');
|
||
|
|
dp.style.display = 'block';
|
||
|
|
});
|
||
|
|
|
||
|
|
function applyFilter() {
|
||
|
|
const val = document.getElementById('kind-filter').value;
|
||
|
|
const filtered = val ? allItems.filter(i => i._entry.kind === val) : allItems;
|
||
|
|
items.clear();
|
||
|
|
items.add(filtered);
|
||
|
|
timeline.fit();
|
||
|
|
}
|
||
|
|
|
||
|
|
timeline.fit();
|
||
|
|
</script>
|
||
|
|
{% endblock scripts %}
|