2026-03-13 00:18:14 +00:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
{% block title %}Projects — Ontoref{% endblock title %}
|
|
|
|
|
|
|
|
|
|
{% block head %}
|
|
|
|
|
<script>
|
|
|
|
|
(function(){
|
|
|
|
|
var last = "";
|
|
|
|
|
try { last = localStorage.getItem("ontoref-last-project") || ""; } catch(_) {}
|
|
|
|
|
if (!last) return;
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
var banner = document.getElementById("resume-banner");
|
|
|
|
|
var lbl = document.getElementById("resume-label");
|
|
|
|
|
if (!banner || !lbl) return;
|
|
|
|
|
lbl.textContent = last;
|
|
|
|
|
banner.href = "/ui/" + encodeURIComponent(last) + "/";
|
|
|
|
|
banner.classList.remove("hidden");
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
{% endblock head %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<!-- Resume last project -->
|
|
|
|
|
<a id="resume-banner" href="#" class="hidden mb-4 flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary/10 border border-primary/20 hover:bg-primary/20 transition-colors text-sm font-medium">
|
|
|
|
|
<svg class="w-4 h-4 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Resume <span id="resume-label" class="font-mono text-primary"></span>
|
|
|
|
|
<span class="ml-auto text-xs text-base-content/40">last project</span>
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<div class="mb-6 flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-2xl font-bold">Projects</h1>
|
|
|
|
|
<p class="text-base-content/50 text-sm mt-1">{{ projects | length }} project{% if projects | length != 1 %}s{% endif %} registered</p>
|
|
|
|
|
</div>
|
|
|
|
|
<a href="/ui/manage" class="btn btn-sm btn-ghost gap-1.5">
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Manage
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{% if projects %}
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
|
|
{% for p in projects %}
|
|
|
|
|
<div class="card bg-base-200 border border-base-content/10">
|
|
|
|
|
<!-- Card header: always visible -->
|
|
|
|
|
<div class="card-body gap-0 py-4 px-5">
|
|
|
|
|
|
|
|
|
|
<!-- Title row -->
|
|
|
|
|
<div class="flex items-start justify-between gap-2 mb-1">
|
|
|
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
|
|
|
<a href="/ui/{{ p.slug }}/"
|
|
|
|
|
class="card-title text-base font-mono link link-hover text-primary">{{ p.slug }}</a>
|
|
|
|
|
{% if p.auth %}
|
|
|
|
|
<span class="badge badge-warning badge-xs flex-shrink-0">protected</span>
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="badge badge-neutral badge-xs flex-shrink-0">open</span>
|
|
|
|
|
{% endif %}
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
{% if p.opmode == "daemon" %}
|
|
|
|
|
<span class="badge badge-success badge-xs flex-shrink-0 gap-1">
|
|
|
|
|
<span class="w-1 h-1 rounded-full bg-current inline-block"></span>daemon
|
|
|
|
|
</span>
|
|
|
|
|
{% elif p.opmode == "push" %}
|
|
|
|
|
<span class="badge badge-info badge-xs flex-shrink-0">push</span>
|
|
|
|
|
{% endif %}
|
2026-03-13 00:18:14 +00:00
|
|
|
{% if p.default_mode %}
|
|
|
|
|
<span class="badge badge-ghost badge-xs flex-shrink-0 font-mono">{{ p.default_mode }}</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if p.repo_kind %}
|
|
|
|
|
<span class="badge badge-outline badge-xs flex-shrink-0 text-base-content/40">{{ p.repo_kind }}</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Quick-access shortcut icons -->
|
|
|
|
|
<div class="flex items-center gap-0.5 flex-shrink-0">
|
2026-03-16 01:48:17 +00:00
|
|
|
{% if p.card %}
|
|
|
|
|
<button onclick="openCard('{{ p.slug }}')" title="Project card"
|
2026-03-29 08:32:50 +01:00
|
|
|
class="btn btn-ghost btn-xs">
|
2026-03-16 01:48:17 +00:00
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
{% endif %}
|
2026-03-13 00:18:14 +00:00
|
|
|
<a href="/ui/{{ p.slug }}/search" title="Search"
|
2026-03-29 08:32:50 +01:00
|
|
|
class="btn btn-ghost btn-xs">
|
2026-03-13 00:18:14 +00:00
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
<a href="/ui/{{ p.slug }}/backlog" title="Backlog"
|
2026-03-29 08:32:50 +01:00
|
|
|
class="btn btn-ghost btn-xs">
|
2026-03-13 00:18:14 +00:00
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
<a href="/ui/{{ p.slug }}/" title="Open dashboard"
|
2026-03-29 08:32:50 +01:00
|
|
|
class="btn btn-ghost btn-xs">
|
2026-03-13 00:18:14 +00:00
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-16 01:48:17 +00:00
|
|
|
{% if p.card and p.card.tagline %}
|
|
|
|
|
<p class="text-sm text-base-content/60 italic leading-snug mb-1.5">{{ p.card.tagline }}</p>
|
|
|
|
|
{% elif p.description %}
|
2026-03-13 00:18:14 +00:00
|
|
|
<p class="text-sm text-base-content/70 leading-snug mb-1.5">{{ p.description }}</p>
|
|
|
|
|
{% endif %}
|
|
|
|
|
<p class="text-xs font-mono text-base-content/35 truncate mb-2" title="{{ p.root }}">{{ p.root }}</p>
|
|
|
|
|
|
|
|
|
|
<!-- Git remotes -->
|
|
|
|
|
{% if p.repos %}
|
|
|
|
|
<div class="flex flex-wrap gap-1 mb-2">
|
|
|
|
|
{% for r in p.repos %}
|
|
|
|
|
<a href="{{ r.url }}" target="_blank" rel="noopener"
|
|
|
|
|
class="badge badge-xs badge-ghost font-mono gap-1 border border-base-content/10 hover:border-primary/40 hover:text-primary transition-colors"
|
|
|
|
|
title="{{ r.url }}">
|
|
|
|
|
<svg class="w-2.5 h-2.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
|
|
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{{ r.name }}
|
|
|
|
|
</a>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
<!-- Showcase + Generated links -->
|
|
|
|
|
{% if p.showcase or p.generated %}
|
|
|
|
|
<div class="flex flex-wrap gap-1.5 mb-2">
|
|
|
|
|
{% for s in p.showcase %}
|
|
|
|
|
<a href="{{ s.url }}" target="_blank" rel="noopener"
|
|
|
|
|
class="btn btn-xs btn-ghost gap-1 border border-base-content/10">
|
|
|
|
|
{% if s.id == "branding" %}🎨{% elif s.id == "web" %}🌐{% elif s.id == "presentation" %}📊{% endif %}
|
|
|
|
|
{{ s.label }}
|
|
|
|
|
</a>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
{% for g in p.generated %}
|
|
|
|
|
<a href="{{ g.url }}" target="_blank" rel="noopener"
|
|
|
|
|
class="btn btn-xs btn-ghost gap-1 border border-base-content/10 opacity-70">
|
|
|
|
|
📄 {{ g.label }}
|
|
|
|
|
</a>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
<!-- Summary badges -->
|
|
|
|
|
<div class="flex flex-wrap gap-1.5 mb-3">
|
|
|
|
|
{% if p.session_count > 0 %}
|
|
|
|
|
<span class="badge badge-sm badge-success gap-1">
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-current inline-block"></span>
|
|
|
|
|
{{ p.session_count }} session{% if p.session_count != 1 %}s{% endif %}
|
|
|
|
|
</span>
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="badge badge-sm badge-ghost">no sessions</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.notif_count > 0 %}
|
|
|
|
|
<span class="badge badge-sm badge-warning">{{ p.notif_count }} notif</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.backlog_open > 0 %}
|
|
|
|
|
<span class="badge badge-sm badge-info">{{ p.backlog_open }} open</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.layers %}
|
|
|
|
|
<span class="badge badge-sm badge-ghost">{{ p.layers | length }} layers</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.op_modes %}
|
|
|
|
|
<span class="badge badge-sm badge-ghost">{{ p.op_modes | length }} modes</span>
|
|
|
|
|
{% endif %}
|
2026-03-29 08:32:50 +01:00
|
|
|
|
|
|
|
|
<span id="mig-badge-{{ p.slug }}" class="badge badge-sm badge-warning hidden"></span>
|
2026-03-13 00:18:14 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Accordion panels -->
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
2026-03-29 08:32:50 +01:00
|
|
|
<div id="mig-panel-{{ p.slug }}"
|
|
|
|
|
hx-get="/ui/{{ p.slug }}/migrations/pending"
|
|
|
|
|
hx-trigger="revealed"
|
|
|
|
|
hx-swap="innerHTML"></div>
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
{% if p.layers or p.op_modes %}
|
|
|
|
|
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
|
|
|
|
|
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">
|
|
|
|
|
Features & Layers
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="collapse-content px-3 pb-3">
|
|
|
|
|
{% if p.layers %}
|
|
|
|
|
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1.5">Layers</p>
|
|
|
|
|
<div class="space-y-1 mb-3">
|
|
|
|
|
{% for l in p.layers %}
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span class="badge badge-xs badge-ghost font-mono flex-shrink-0 mt-0.5">{{ l.id }}</span>
|
|
|
|
|
<span class="text-xs text-base-content/70">{{ l.description }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if p.op_modes %}
|
|
|
|
|
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1.5">Operational Modes</p>
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
{% for m in p.op_modes %}
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<span class="badge badge-xs badge-primary font-mono flex-shrink-0 mt-0.5">{{ m.id }}</span>
|
|
|
|
|
<span class="text-xs text-base-content/70">{{ m.description }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.backlog_items %}
|
|
|
|
|
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
|
|
|
|
|
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">
|
|
|
|
|
Backlog
|
|
|
|
|
<span class="badge badge-xs badge-info ml-1">{{ p.backlog_open }} open</span>
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="collapse-content px-3 pb-3">
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
{% for it in p.backlog_items %}
|
|
|
|
|
<div class="flex items-start gap-1.5">
|
|
|
|
|
<span class="badge badge-xs font-mono flex-shrink-0 mt-0.5
|
|
|
|
|
{% if it.status == 'Open' %}badge-info
|
|
|
|
|
{% elif it.status == 'Done' %}badge-success
|
|
|
|
|
{% else %}badge-ghost{% endif %}">
|
|
|
|
|
{{ it.status }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="badge badge-xs flex-shrink-0 mt-0.5
|
|
|
|
|
{% if it.priority == 'Critical' %}badge-error
|
|
|
|
|
{% elif it.priority == 'High' %}badge-warning
|
|
|
|
|
{% else %}badge-ghost{% endif %}">
|
|
|
|
|
{{ it.priority }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-xs text-base-content/80 leading-tight">{{ it.title }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
<a href="/ui/{{ p.slug }}/backlog" class="btn btn-xs btn-ghost mt-2 w-full">
|
|
|
|
|
Manage backlog →
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.sessions %}
|
|
|
|
|
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
|
|
|
|
|
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">
|
|
|
|
|
Sessions
|
|
|
|
|
<span class="badge badge-xs badge-success ml-1">{{ p.session_count }}</span>
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="collapse-content px-3 pb-3">
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
{% for s in p.sessions %}
|
|
|
|
|
<div class="flex items-center gap-1.5 text-xs">
|
|
|
|
|
<span class="badge badge-xs badge-ghost font-mono">{{ s.actor_type }}</span>
|
|
|
|
|
<span class="text-base-content/60">{{ s.hostname }}</span>
|
|
|
|
|
<span class="text-base-content/30 ml-auto">{{ s.last_seen_ago }}s ago</span>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if p.notifications %}
|
|
|
|
|
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
|
|
|
|
|
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">
|
|
|
|
|
Notifications
|
|
|
|
|
<span class="badge badge-xs badge-warning ml-1">{{ p.notif_count }}</span>
|
|
|
|
|
</summary>
|
|
|
|
|
<div class="collapse-content px-3 pb-3">
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
{% for n in p.notifications %}
|
|
|
|
|
<div class="flex items-start gap-1.5 text-xs">
|
|
|
|
|
<span class="badge badge-xs badge-ghost flex-shrink-0">{{ n.event }}</span>
|
|
|
|
|
<span class="text-base-content/60 truncate">
|
|
|
|
|
{% if n.files %}
|
|
|
|
|
{{ n.files | first }}
|
|
|
|
|
{% set fc = n.files | length %}
|
|
|
|
|
{% if fc > 1 %} +{{ fc - 1 }}{% endif %}
|
|
|
|
|
{% endif %}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="text-base-content/30 ml-auto flex-shrink-0">{{ n.age_secs }}s</span>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% else %}
|
|
|
|
|
<div class="flex flex-col items-center justify-center py-16 text-base-content/40">
|
|
|
|
|
<svg class="w-12 h-12 mb-3 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
|
|
|
|
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<p class="text-sm">No projects registered.</p>
|
|
|
|
|
<a href="/ui/manage" class="btn btn-sm btn-ghost mt-3">Add a project</a>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
2026-03-16 01:48:17 +00:00
|
|
|
|
|
|
|
|
<!-- Card modal -->
|
|
|
|
|
<dialog id="card-modal" class="modal">
|
|
|
|
|
<div class="modal-box w-11/12 max-w-lg">
|
|
|
|
|
<div class="flex items-start justify-between mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="font-bold text-lg font-mono" id="card-modal-slug"></h3>
|
|
|
|
|
<p class="text-sm text-base-content/50 italic mt-0.5" id="card-modal-tagline"></p>
|
|
|
|
|
</div>
|
|
|
|
|
<form method="dialog">
|
|
|
|
|
<button class="btn btn-sm btn-circle btn-ghost">✕</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-3 text-sm" id="card-modal-body"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
|
|
|
</dialog>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
var CARDS = {
|
|
|
|
|
{% for p in projects %}{% if p.card %}
|
|
|
|
|
"{{ p.slug }}": {
|
|
|
|
|
tagline: {{ p.card.tagline | json_encode | safe }},
|
|
|
|
|
description: {{ p.card.description | json_encode | safe }},
|
|
|
|
|
version: {{ p.card.version | json_encode | safe }},
|
|
|
|
|
status: {{ p.card.status | json_encode | safe }},
|
|
|
|
|
url: {{ p.card.url | json_encode | safe }},
|
|
|
|
|
tags: {{ p.card.tags | json_encode | safe }},
|
|
|
|
|
features: {{ p.card.features | json_encode | safe }},
|
|
|
|
|
},
|
|
|
|
|
{% endif %}{% endfor %}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.openCard = function(slug) {
|
|
|
|
|
var c = CARDS[slug];
|
|
|
|
|
if (!c) return;
|
|
|
|
|
document.getElementById("card-modal-slug").textContent = slug;
|
|
|
|
|
document.getElementById("card-modal-tagline").textContent = c.tagline;
|
|
|
|
|
var body = document.getElementById("card-modal-body");
|
|
|
|
|
var html = "";
|
|
|
|
|
if (c.description) {
|
|
|
|
|
html += '<p class="text-base-content/80 leading-relaxed">' + c.description + '</p>';
|
|
|
|
|
}
|
|
|
|
|
var meta = [];
|
|
|
|
|
if (c.version) meta.push('<span class="badge badge-ghost badge-sm font-mono">v' + c.version + '</span>');
|
|
|
|
|
if (c.status) meta.push('<span class="badge badge-outline badge-sm">' + c.status + '</span>');
|
|
|
|
|
if (meta.length) html += '<div class="flex gap-2 flex-wrap">' + meta.join("") + '</div>';
|
|
|
|
|
if (c.features && c.features.length) {
|
|
|
|
|
html += '<div><p class="text-xs font-semibold text-base-content/40 uppercase tracking-wider mb-1.5">Features</p><ul class="space-y-1">';
|
|
|
|
|
c.features.forEach(function(f) {
|
|
|
|
|
html += '<li class="flex gap-2 text-xs text-base-content/70"><span class="text-primary flex-shrink-0">▸</span>' + f + '</li>';
|
|
|
|
|
});
|
|
|
|
|
html += '</ul></div>';
|
|
|
|
|
}
|
|
|
|
|
if (c.tags && c.tags.length) {
|
|
|
|
|
html += '<div class="flex flex-wrap gap-1.5">';
|
|
|
|
|
c.tags.forEach(function(t) {
|
|
|
|
|
html += '<span class="badge badge-xs badge-ghost font-mono">' + t + '</span>';
|
|
|
|
|
});
|
|
|
|
|
html += '</div>';
|
|
|
|
|
}
|
|
|
|
|
if (c.url) {
|
|
|
|
|
html += '<a href="' + c.url + '" target="_blank" rel="noopener" class="btn btn-xs btn-ghost gap-1 self-start border border-base-content/10">↗ ' + c.url + '</a>';
|
|
|
|
|
}
|
|
|
|
|
body.innerHTML = html;
|
|
|
|
|
document.getElementById("card-modal").showModal();
|
|
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
2026-03-13 00:18:14 +00:00
|
|
|
{% endblock content %}
|