feat: replace basic frontmatter display with structured accent card UI featuring badges, tags, and boolean flags

This commit is contained in:
Bruno Charest 2026-03-24 19:29:09 -04:00
parent d020264ba3
commit dc2fdbe109
3 changed files with 597 additions and 44 deletions

View File

@ -2183,20 +2183,10 @@
window.open(popoutUrl, `popout_${data.vault}_${data.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no'); window.open(popoutUrl, `popout_${data.vault}_${data.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no');
}); });
// Frontmatter // Frontmatter — Accent Card
let fmSection = null; let fmSection = null;
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) { if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
const fmToggle = el("div", { class: "frontmatter-toggle" }, [ fmSection = buildFrontmatterCard(data.frontmatter);
document.createTextNode("▶ Frontmatter"),
]);
const fmContent = el("div", { class: "frontmatter-content" }, [
document.createTextNode(JSON.stringify(data.frontmatter, null, 2)),
]);
fmToggle.addEventListener("click", () => {
fmContent.classList.toggle("open");
fmToggle.textContent = fmContent.classList.contains("open") ? "▼ Frontmatter" : "▶ Frontmatter";
});
fmSection = el("div", {}, [fmToggle, fmContent]);
} }
// Content container (rendered HTML) // Content container (rendered HTML)
@ -3419,6 +3409,186 @@
}); });
} }
// ---------------------------------------------------------------------------
// Frontmatter Accent Card Builder
// ---------------------------------------------------------------------------
function buildFrontmatterCard(frontmatter) {
// Helper: format date
function formatDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
const date = d.toISOString().slice(0, 10);
const time = d.toTimeString().slice(0, 5);
return `${date} · ${time}`;
}
// Extract boolean flags
const booleanFlags = ['publish', 'favoris', 'template', 'task', 'archive', 'draft', 'private']
.map(key => ({ key, value: !!frontmatter[key] }));
// Toggle state
let isOpen = true;
// Build header with chevron
const chevron = el("span", { class: "fm-chevron open" });
chevron.innerHTML = '<i data-lucide="chevron-down" style="width:14px;height:14px"></i>';
const fmHeader = el("div", { class: "fm-header" }, [
chevron,
document.createTextNode("Frontmatter")
]);
// ZONE 1: Top strip
const topBadges = [];
// Title badge
const title = frontmatter.titre || frontmatter.title || '';
if (title) {
topBadges.push(el("span", { class: "ac-title" }, [document.createTextNode(`"${title}"`)]));
}
// Status badge
if (frontmatter.statut) {
const statusBadge = el("span", { class: "ac-badge green" }, [
el("span", { class: "ac-dot" }),
document.createTextNode(frontmatter.statut)
]);
topBadges.push(statusBadge);
}
// Category badge
if (frontmatter.catégorie || frontmatter.categorie) {
const cat = frontmatter.catégorie || frontmatter.categorie;
const catBadge = el("span", { class: "ac-badge blue" }, [
document.createTextNode(cat)
]);
topBadges.push(catBadge);
}
// Publish badge
if (frontmatter.publish) {
topBadges.push(el("span", { class: "ac-badge purple" }, [
document.createTextNode("publié")
]));
}
// Favoris badge
if (frontmatter.favoris) {
topBadges.push(el("span", { class: "ac-badge purple" }, [
document.createTextNode("favori")
]));
}
const acTop = el("div", { class: "ac-top" }, topBadges);
// ZONE 2: Body 2 columns
const leftCol = el("div", { class: "ac-col" }, [
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("auteur")]),
el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || '—')])
]),
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]),
el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie || '—')])
]),
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("statut")]),
el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || '—')])
]),
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("aliases")]),
el("span", { class: "ac-v muted" }, [
document.createTextNode(
frontmatter.aliases && frontmatter.aliases.length > 0
? frontmatter.aliases.join(', ')
: '[]'
)
])
])
]);
const rightCol = el("div", { class: "ac-col" }, [
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]),
el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))])
]),
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]),
el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))])
]),
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("publish")]),
el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))])
]),
el("div", { class: "ac-row" }, [
el("span", { class: "ac-k" }, [document.createTextNode("favoris")]),
el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))])
])
]);
const acBody = el("div", { class: "ac-body" }, [leftCol, rightCol]);
// ZONE 3: Tags row
const tagPills = [];
if (frontmatter.tags && frontmatter.tags.length > 0) {
frontmatter.tags.forEach(tag => {
tagPills.push(el("span", { class: "ac-tag" }, [document.createTextNode(tag)]));
});
}
const acTagsRow = el("div", { class: "ac-tags-row" }, [
el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]),
el("div", { class: "ac-tags-wrap" }, tagPills)
]);
// ZONE 4: Flags row
const flagChips = [];
booleanFlags.forEach(flag => {
const chipClass = flag.value ? "flag-chip on" : "flag-chip off";
flagChips.push(el("span", { class: chipClass }, [
el("span", { class: "flag-dot" }),
document.createTextNode(flag.key)
]));
});
const acFlagsRow = el("div", { class: "ac-flags-row" }, [
el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]),
...flagChips
]);
// Assemble the card
const acCard = el("div", { class: "ac-card" }, [
acTop,
acBody,
acTagsRow,
acFlagsRow
]);
// Toggle functionality
fmHeader.addEventListener("click", () => {
isOpen = !isOpen;
if (isOpen) {
acCard.style.display = "block";
chevron.classList.remove("closed");
chevron.classList.add("open");
} else {
acCard.style.display = "none";
chevron.classList.remove("open");
chevron.classList.add("closed");
}
safeCreateIcons();
});
// Wrap in section
const fmSection = el("div", { class: "fm-section" }, [
fmHeader,
acCard
]);
return fmSection;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -135,23 +135,9 @@
header.appendChild(actionsDiv); header.appendChild(actionsDiv);
area.appendChild(header); area.appendChild(header);
// Frontmatter // Frontmatter — Accent Card
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) { if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
const fmSection = document.createElement("div"); const fmSection = buildFrontmatterCard(data.frontmatter);
const fmToggle = document.createElement("div");
fmToggle.className = "frontmatter-toggle";
fmToggle.textContent = "▶ Frontmatter";
const fmContent = document.createElement("div");
fmContent.className = "frontmatter-content";
fmContent.textContent = JSON.stringify(data.frontmatter, null, 2);
fmToggle.addEventListener("click", () => {
fmContent.classList.toggle("open");
fmToggle.textContent = fmContent.classList.contains("open") ? "▼ Frontmatter" : "▶ Frontmatter";
});
fmSection.appendChild(fmToggle);
fmSection.appendChild(fmContent);
area.appendChild(fmSection); area.appendChild(fmSection);
} }
@ -197,6 +183,186 @@
return item; return item;
} }
function buildFrontmatterCard(frontmatter) {
// Helper: format date
function formatDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
const date = d.toISOString().slice(0, 10);
const time = d.toTimeString().slice(0, 5);
return `${date} · ${time}`;
}
// Helper: create element
function el(tag, className, children) {
const e = document.createElement(tag);
if (className) e.className = className;
if (children) {
children.forEach(c => { if (c) e.appendChild(c); });
}
return e;
}
// Extract boolean flags
const booleanFlags = ['publish', 'favoris', 'template', 'task', 'archive', 'draft', 'private']
.map(key => ({ key, value: !!frontmatter[key] }));
// Toggle state
let isOpen = true;
// Build header with chevron
const chevron = el("span", "fm-chevron open");
chevron.innerHTML = '<i data-lucide="chevron-down" style="width:14px;height:14px"></i>';
const fmHeader = el("div", "fm-header");
fmHeader.appendChild(chevron);
fmHeader.appendChild(document.createTextNode("Frontmatter"));
// ZONE 1: Top strip
const acTop = el("div", "ac-top");
// Title badge
const title = frontmatter.titre || frontmatter.title || '';
if (title) {
const titleSpan = el("span", "ac-title");
titleSpan.textContent = `"${title}"`;
acTop.appendChild(titleSpan);
}
// Status badge
if (frontmatter.statut) {
const statusBadge = el("span", "ac-badge green");
const dot = el("span", "ac-dot");
statusBadge.appendChild(dot);
statusBadge.appendChild(document.createTextNode(frontmatter.statut));
acTop.appendChild(statusBadge);
}
// Category badge
if (frontmatter.catégorie || frontmatter.categorie) {
const cat = frontmatter.catégorie || frontmatter.categorie;
const catBadge = el("span", "ac-badge blue");
catBadge.textContent = cat;
acTop.appendChild(catBadge);
}
// Publish badge
if (frontmatter.publish) {
const pubBadge = el("span", "ac-badge purple");
pubBadge.textContent = "publié";
acTop.appendChild(pubBadge);
}
// Favoris badge
if (frontmatter.favoris) {
const favBadge = el("span", "ac-badge purple");
favBadge.textContent = "favori";
acTop.appendChild(favBadge);
}
// ZONE 2: Body 2 columns
const leftCol = el("div", "ac-col");
const row1 = el("div", "ac-row");
row1.innerHTML = '<span class="ac-k">auteur</span><span class="ac-v">' + (frontmatter.auteur || '—') + '</span>';
leftCol.appendChild(row1);
const row2 = el("div", "ac-row");
row2.innerHTML = '<span class="ac-k">catégorie</span><span class="ac-v">' + (frontmatter.catégorie || frontmatter.categorie || '—') + '</span>';
leftCol.appendChild(row2);
const row3 = el("div", "ac-row");
row3.innerHTML = '<span class="ac-k">statut</span><span class="ac-v">' + (frontmatter.statut || '—') + '</span>';
leftCol.appendChild(row3);
const row4 = el("div", "ac-row");
const aliases = frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(', ') : '[]';
row4.innerHTML = '<span class="ac-k">aliases</span><span class="ac-v muted">' + aliases + '</span>';
leftCol.appendChild(row4);
const rightCol = el("div", "ac-col");
const row5 = el("div", "ac-row");
row5.innerHTML = '<span class="ac-k">creation_date</span><span class="ac-v mono">' + formatDate(frontmatter.creation_date) + '</span>';
rightCol.appendChild(row5);
const row6 = el("div", "ac-row");
row6.innerHTML = '<span class="ac-k">modification_date</span><span class="ac-v mono">' + formatDate(frontmatter.modification_date) + '</span>';
rightCol.appendChild(row6);
const row7 = el("div", "ac-row");
row7.innerHTML = '<span class="ac-k">publish</span><span class="ac-v">' + String(frontmatter.publish || false) + '</span>';
rightCol.appendChild(row7);
const row8 = el("div", "ac-row");
row8.innerHTML = '<span class="ac-k">favoris</span><span class="ac-v">' + String(frontmatter.favoris || false) + '</span>';
rightCol.appendChild(row8);
const acBody = el("div", "ac-body");
acBody.appendChild(leftCol);
acBody.appendChild(rightCol);
// ZONE 3: Tags row
const acTagsRow = el("div", "ac-tags-row");
const tagsLabel = el("span", "ac-tags-k");
tagsLabel.textContent = "tags";
acTagsRow.appendChild(tagsLabel);
const tagsWrap = el("div", "ac-tags-wrap");
if (frontmatter.tags && frontmatter.tags.length > 0) {
frontmatter.tags.forEach(tag => {
const tagSpan = el("span", "ac-tag");
tagSpan.textContent = tag;
tagsWrap.appendChild(tagSpan);
});
}
acTagsRow.appendChild(tagsWrap);
// ZONE 4: Flags row
const acFlagsRow = el("div", "ac-flags-row");
const flagsLabel = el("span", "ac-flags-k");
flagsLabel.textContent = "flags";
acFlagsRow.appendChild(flagsLabel);
booleanFlags.forEach(flag => {
const chipClass = flag.value ? "flag-chip on" : "flag-chip off";
const chip = el("span", chipClass);
const dot = el("span", "flag-dot");
chip.appendChild(dot);
chip.appendChild(document.createTextNode(flag.key));
acFlagsRow.appendChild(chip);
});
// Assemble the card
const acCard = el("div", "ac-card");
acCard.appendChild(acTop);
acCard.appendChild(acBody);
acCard.appendChild(acTagsRow);
acCard.appendChild(acFlagsRow);
// Toggle functionality
fmHeader.addEventListener("click", () => {
isOpen = !isOpen;
if (isOpen) {
acCard.style.display = "block";
chevron.classList.remove("closed");
chevron.classList.add("open");
} else {
acCard.style.display = "none";
chevron.classList.remove("open");
chevron.classList.add("closed");
}
if (window.lucide) window.lucide.createIcons();
});
// Wrap in section
const fmSection = el("div", "fm-section");
fmSection.appendChild(fmHeader);
fmSection.appendChild(acCard);
return fmSection;
}
window.onload = initPopout; window.onload = initPopout;
</script> </script>
</body> </body>

View File

@ -30,6 +30,25 @@
--danger-bg: #3d1a18; --danger-bg: #3d1a18;
--success: #3fb950; --success: #3fb950;
--success-bg: #1a3d1f; --success-bg: #1a3d1f;
/* Accent Card variables */
--accent-card: #58a6ff;
--accent-bg: #1f2a3a;
--accent-text: #79c0ff;
--green: #3fb950;
--green-bg: #1a3d1f;
--purple: #a371f7;
--purple-bg: #2d1b4e;
--surface: #161b22;
--surface2: #0f1117;
--surface3: #0d0f14;
--border-md: #30363d;
--text: #e6edf3;
--text-2: #8b949e;
--text-3: #484f58;
--mono: 'JetBrains Mono', monospace;
--radius: 6px;
--radius-lg: 10px;
} }
/* ===== THEME — LIGHT ===== */ /* ===== THEME — LIGHT ===== */
@ -55,6 +74,25 @@
--danger-bg: #ffebe9; --danger-bg: #ffebe9;
--success: #1a7f37; --success: #1a7f37;
--success-bg: #dafbe1; --success-bg: #dafbe1;
/* Accent Card variables */
--accent-card: #4f6ef7;
--accent-bg: #eef1fe;
--accent-text: #2540c9;
--green: #1a7f50;
--green-bg: #eaf5ee;
--purple: #6b40c4;
--purple-bg: #f1ecfd;
--surface: #ffffff;
--surface2: #f2f1ef;
--surface3: #ebe9e6;
--border-md: rgba(0,0,0,0.13);
--text: #1a1917;
--text-2: #6b6a67;
--text-3: #a09e9b;
--mono: 'JetBrains Mono', monospace;
--radius: 6px;
--radius-lg: 10px;
} }
/* ===== BASE ===== */ /* ===== BASE ===== */
@ -1061,35 +1099,214 @@ select {
background: var(--tag-bg); background: var(--tag-bg);
} }
/* Frontmatter collapsible */ /* ===== FRONTMATTER — ACCENT CARD ===== */
.frontmatter-toggle {
/* Frontmatter section wrapper */
.fm-section {
margin-bottom: 20px;
}
/* Header toggle */
.fm-header {
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem; font-size: 0.78rem;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
margin-bottom: 12px; margin-bottom: 12px;
user-select: none; user-select: none;
transition: color 150ms ease;
} }
.frontmatter-toggle:hover { .fm-header:hover {
color: var(--text-secondary); color: var(--text-secondary);
} }
.frontmatter-content {
background: var(--code-bg); /* Chevron icon */
border: 1px solid var(--border); .fm-chevron {
border-radius: 8px; transition: transform 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.fm-chevron.open {
transform: rotate(0deg);
}
.fm-chevron.closed {
transform: rotate(-90deg);
}
/* Accent Card main container */
.ac-card {
border: 1px solid var(--border-md);
border-left: 3px solid var(--accent-card);
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
background: var(--surface);
overflow: hidden;
}
/* ZONE 1 — Top strip */
.ac-top {
padding: 12px 16px 10px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.ac-title {
font-size: 14px;
font-weight: 600;
color: var(--text);
font-family: var(--mono);
letter-spacing: -0.01em;
flex-shrink: 0;
}
.ac-badge {
padding: 2px 9px;
border-radius: 99px;
font-size: 11px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 4px;
}
.ac-badge.green {
background: var(--green-bg);
color: var(--green);
}
.ac-badge.blue {
background: var(--accent-bg);
color: var(--accent-text);
}
.ac-badge.purple {
background: var(--purple-bg);
color: var(--purple);
}
.ac-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
opacity: 0.7;
}
/* ZONE 2 — Body 2 columns */
.ac-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.ac-col {
padding: 12px 16px; padding: 12px 16px;
margin-bottom: 20px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--text-secondary);
white-space: pre-wrap;
display: none;
} }
.frontmatter-content.open { .ac-col + .ac-col {
display: block; border-left: 1px solid var(--border);
}
.ac-row {
display: flex;
align-items: baseline;
gap: 6px;
padding: 4px 0;
font-size: 12.5px;
}
.ac-row + .ac-row {
border-top: 1px solid var(--border);
}
.ac-k {
color: var(--text-3);
font-size: 11px;
font-family: var(--mono);
min-width: 110px;
flex-shrink: 0;
}
.ac-v {
color: var(--text);
font-weight: 500;
font-size: 12.5px;
}
.ac-v.mono {
font-family: var(--mono);
font-weight: 400;
color: var(--text-2);
font-size: 11px;
}
.ac-v.muted {
color: var(--text-3);
font-weight: 400;
}
/* ZONE 3 — Tags row */
.ac-tags-row {
padding: 10px 16px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.ac-tags-k {
font-size: 11px;
font-family: var(--mono);
color: var(--text-3);
flex-shrink: 0;
}
.ac-tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.ac-tag {
padding: 2px 9px;
border-radius: 99px;
font-size: 11px;
font-weight: 500;
background: var(--accent-bg);
color: var(--accent-text);
border: 1px solid rgba(79, 110, 247, 0.18);
}
/* ZONE 4 — Flags row */
.ac-flags-row {
padding: 9px 16px;
border-top: 1px solid var(--border);
background: var(--surface2);
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.ac-flags-k {
font-size: 11px;
font-family: var(--mono);
color: var(--text-3);
margin-right: 2px;
}
.flag-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius);
font-size: 11px;
font-weight: 500;
border: 1px solid var(--border-md);
}
.flag-chip.on {
background: var(--green-bg);
color: var(--green);
border-color: rgba(26, 127, 80, 0.2);
}
.flag-chip.off {
background: var(--surface3);
color: var(--text-3);
}
.flag-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
opacity: 0.7;
} }
/* --- Raw source view --- */ /* --- Raw source view --- */