ontoref-code/crates/ontoref-daemon/templates/pages/timeline.html

136 lines
4.9 KiB
HTML
Raw Normal View History

{% 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 %}