feat: first take on sidebars

This commit is contained in:
2026-04-25 20:26:55 +02:00
parent 7ebea742a5
commit 55b3071696
11 changed files with 951 additions and 168 deletions

View File

@@ -65,7 +65,12 @@
"Command failed": "Befehl fehlgeschlagen",
"Command failed with HTTP %{status}": "Befehl mit HTTP %{status} fehlgeschlagen",
"Create Project": "Projekt erstellen",
"Create / Edit": "Erstellen / Bearbeiten",
"Content": "Inhalte",
"Data": "Daten",
"Dashboard": "Instrumententafel",
"AI conversations": "KI-Gespräche",
"Automation helpers": "Automatisierungshilfen",
"dashboard.postCount.one": "%{count} Beitrag",
"dashboard.postCount.other": "%{count} Beiträge",
"dashboard.section.categories": "Kategorien",
@@ -87,6 +92,20 @@
"dashboard.tagCloud.more": "+%{count} weitere",
"dashboard.title": "Übersicht",
"Desktop Runtime": "Desktop-Laufzeit",
"Editor": "Editor",
"Images and files": "Bilder und Dateien",
"Import definitions": "Importdefinitionen",
"Merge Tags": "Tags zusammenführen",
"Project": "Projekt",
"Project and publishing": "Projekt und Veröffentlichung",
"Publishing": "Veröffentlichung",
"Site rendering": "Website-Rendering",
"Standalone pages": "Eigenständige Seiten",
"Tag Cloud": "Tag-Wolke",
"Tag management": "Tag-Verwaltung",
"Technology": "Technik",
"Working tree": "Arbeitsverzeichnis",
"Working tree and history": "Arbeitsverzeichnis und Verlauf",
"Desktop workbench content routed through the Elixir shell.": "Desktop-Arbeitsbereichsinhalte werden durch die Elixir-Shell geleitet.",
"Desktop workbench shell wired through Elixir": "Desktop-Workbench-Shell über Elixir verdrahtet",
"Diff Reports": "Diff-Berichte",

View File

@@ -65,7 +65,12 @@
"Command failed": "Command failed",
"Command failed with HTTP %{status}": "Command failed with HTTP %{status}",
"Create Project": "Create Project",
"Create / Edit": "Create / Edit",
"Content": "Content",
"Data": "Data",
"Dashboard": "Dashboard",
"AI conversations": "AI conversations",
"Automation helpers": "Automation helpers",
"dashboard.postCount.one": "%{count} post",
"dashboard.postCount.other": "%{count} posts",
"dashboard.section.categories": "Categories",
@@ -87,6 +92,20 @@
"dashboard.tagCloud.more": "+%{count} more",
"dashboard.title": "Dashboard",
"Desktop Runtime": "Desktop Runtime",
"Editor": "Editor",
"Images and files": "Images and files",
"Import definitions": "Import definitions",
"Merge Tags": "Merge Tags",
"Project": "Project",
"Project and publishing": "Project and publishing",
"Publishing": "Publishing",
"Site rendering": "Site rendering",
"Standalone pages": "Standalone pages",
"Tag Cloud": "Tag Cloud",
"Tag management": "Tag management",
"Technology": "Technology",
"Working tree": "Working tree",
"Working tree and history": "Working tree and history",
"Desktop workbench content routed through the Elixir shell.": "Desktop workbench content routed through the Elixir shell.",
"Desktop workbench shell wired through Elixir": "Desktop workbench shell wired through Elixir",
"Diff Reports": "Diff Reports",

View File

@@ -65,7 +65,12 @@
"Command failed": "El comando falló",
"Command failed with HTTP %{status}": "El comando falló con HTTP %{status}",
"Create Project": "Crear proyecto",
"Create / Edit": "Crear / editar",
"Content": "Contenido",
"Data": "Datos",
"Dashboard": "Panel",
"AI conversations": "Conversaciones de IA",
"Automation helpers": "Ayudas de automatización",
"dashboard.postCount.one": "%{count} entrada",
"dashboard.postCount.other": "%{count} entradas",
"dashboard.section.categories": "Categorías",
@@ -87,6 +92,20 @@
"dashboard.tagCloud.more": "+%{count} más",
"dashboard.title": "Panel",
"Desktop Runtime": "Entorno de escritorio",
"Editor": "Editor",
"Images and files": "Imágenes y archivos",
"Import definitions": "Definiciones de importación",
"Merge Tags": "Combinar etiquetas",
"Project": "Proyecto",
"Project and publishing": "Proyecto y publicación",
"Publishing": "Publicación",
"Site rendering": "Renderizado del sitio",
"Standalone pages": "Páginas independientes",
"Tag Cloud": "Nube de etiquetas",
"Tag management": "Gestión de etiquetas",
"Technology": "Tecnología",
"Working tree": "Árbol de trabajo",
"Working tree and history": "Árbol de trabajo e historial",
"Desktop workbench content routed through the Elixir shell.": "El contenido del área de trabajo de escritorio se enruta a través del shell de Elixir.",
"Desktop workbench shell wired through Elixir": "Shell del área de trabajo de escritorio conectado mediante Elixir",
"Diff Reports": "Informes de diff",

View File

@@ -65,7 +65,12 @@
"Command failed": "La commande a échoué",
"Command failed with HTTP %{status}": "La commande a échoué avec HTTP %{status}",
"Create Project": "Créer un projet",
"Create / Edit": "Créer / modifier",
"Content": "Contenu",
"Data": "Données",
"Dashboard": "Tableau de bord",
"AI conversations": "Conversations IA",
"Automation helpers": "Aides dautomatisation",
"dashboard.postCount.one": "%{count} article",
"dashboard.postCount.other": "%{count} articles",
"dashboard.section.categories": "Catégories",
@@ -87,6 +92,20 @@
"dashboard.tagCloud.more": "+%{count} de plus",
"dashboard.title": "Tableau de bord",
"Desktop Runtime": "Exécution bureau",
"Editor": "Éditeur",
"Images and files": "Images et fichiers",
"Import definitions": "Définitions dimport",
"Merge Tags": "Fusionner les tags",
"Project": "Projet",
"Project and publishing": "Projet et publication",
"Publishing": "Publication",
"Site rendering": "Rendu du site",
"Standalone pages": "Pages autonomes",
"Tag Cloud": "Nuage de tags",
"Tag management": "Gestion des tags",
"Technology": "Technologie",
"Working tree": "Arbre de travail",
"Working tree and history": "Arbre de travail et historique",
"Desktop workbench content routed through the Elixir shell.": "Le contenu de latelier bureau est acheminé via le shell Elixir.",
"Desktop workbench shell wired through Elixir": "Shell datelier bureau câblé via Elixir",
"Diff Reports": "Rapports de diff",

View File

@@ -65,7 +65,12 @@
"Command failed": "Comando non riuscito",
"Command failed with HTTP %{status}": "Comando non riuscito con HTTP %{status}",
"Create Project": "Crea progetto",
"Create / Edit": "Crea / modifica",
"Content": "Contenuti",
"Data": "Dati",
"Dashboard": "Dashboard",
"AI conversations": "Conversazioni IA",
"Automation helpers": "Strumenti di automazione",
"dashboard.postCount.one": "%{count} post",
"dashboard.postCount.other": "%{count} post",
"dashboard.section.categories": "Categorie",
@@ -87,6 +92,20 @@
"dashboard.tagCloud.more": "+%{count} in più",
"dashboard.title": "Dashboard",
"Desktop Runtime": "Runtime desktop",
"Editor": "Editor",
"Images and files": "Immagini e file",
"Import definitions": "Definizioni di importazione",
"Merge Tags": "Unisci tag",
"Project": "Progetto",
"Project and publishing": "Progetto e pubblicazione",
"Publishing": "Pubblicazione",
"Site rendering": "Rendering del sito",
"Standalone pages": "Pagine autonome",
"Tag Cloud": "Nuvola di tag",
"Tag management": "Gestione tag",
"Technology": "Tecnologia",
"Working tree": "Working tree",
"Working tree and history": "Working tree e cronologia",
"Desktop workbench content routed through the Elixir shell.": "I contenuti del banco di lavoro desktop vengono instradati tramite la shell Elixir.",
"Desktop workbench shell wired through Elixir": "Shell del banco di lavoro desktop collegata tramite Elixir",
"Diff Reports": "Report diff",

View File

@@ -1441,6 +1441,218 @@ button {
white-space: nowrap;
}
.sidebar-section-title {
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px 8px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.section-icon {
font-size: 11px;
line-height: 1;
}
.section-icon.status-draft {
color: var(--vscode-editorWarning-foreground);
}
.section-icon.status-published {
color: var(--vscode-testing-iconPassed);
}
.section-icon.status-archived {
color: var(--vscode-descriptionForeground);
}
.sidebar-list {
display: flex;
flex-direction: column;
}
.sidebar-post-item {
flex-direction: row;
align-items: flex-start;
gap: 10px;
}
.post-type-icon {
width: 18px;
flex: 0 0 18px;
text-align: center;
line-height: 1.2;
}
.sidebar-item-content {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.sidebar-item-title-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.sidebar-item-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-item-language-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
padding: 0 5px;
border-radius: 999px;
background: rgba(79, 179, 255, 0.14);
color: var(--vscode-titleBar-activeForeground);
font-size: 11px;
}
.sidebar-item-meta {
color: var(--vscode-descriptionForeground);
font-size: 12px;
}
.media-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 0 12px 12px;
}
.media-item {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
padding: 12px;
border: none;
border-radius: 10px;
background: transparent;
color: inherit;
text-align: left;
}
.media-item:hover {
background: var(--vscode-list-hoverBackground);
}
.media-item.selected {
background: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.media-thumbnail {
display: flex;
align-items: center;
justify-content: center;
height: 72px;
border-radius: 8px;
background: var(--vscode-input-background);
font-size: 28px;
}
.media-item-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.media-item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.media-item-size {
color: var(--vscode-descriptionForeground);
font-size: 12px;
}
.chat-list-item {
display: flex;
width: 100%;
padding: 10px 12px;
border: none;
background: transparent;
color: inherit;
text-align: left;
}
.chat-list-item:hover {
background: var(--vscode-list-hoverBackground);
}
.chat-list-item.active {
background: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.chat-item-content {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.chat-item-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-item-date {
color: var(--vscode-descriptionForeground);
font-size: 12px;
}
.settings-nav-list {
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 12px 12px;
}
.settings-nav-entry {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 10px;
background: transparent;
color: inherit;
text-align: left;
}
.settings-nav-entry:hover {
background: var(--vscode-list-hoverBackground);
}
.settings-nav-entry-icon {
width: 18px;
flex: 0 0 18px;
text-align: center;
}
.sidebar-empty {
padding: 16px 12px;
color: var(--vscode-descriptionForeground);
}
@media (max-width: 820px) {
.dashboard-stats {
grid-template-columns: 1fr;
@@ -1450,4 +1662,8 @@ button {
align-items: flex-start;
flex-wrap: wrap;
}
.media-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -144,7 +144,23 @@ function renderSidebar() {
</div>
</div>
<div class="sidebar-content">
${data.sections
${renderSidebarBody(data, view)}
</div>
`;
}
function renderSidebarBody(data, view) {
switch (data.layout) {
case "post_list":
return renderSidebarPostList(data, view);
case "media_grid":
return renderSidebarMediaGrid(data, view);
case "entity_list":
return renderSidebarEntityList(data, view);
case "nav_list":
return renderSidebarNavList(data, view);
default:
return (data.sections || [])
.map(
(section) => `
<section class="sidebar-section">
@@ -152,17 +168,85 @@ function renderSidebar() {
<span data-testid="sidebar-section-title">${escapeHtml(tText(section.title))}</span>
</div>
<div class="sidebar-section-items">
${section.items.map((item) => renderSidebarItem(item, view)).join("")}
${(section.items || []).map((item) => renderSidebarItem(item, view)).join("")}
</div>
</section>
`
)
.join("")}
.join("");
}
}
function renderSidebarPostList(data, view) {
const sections = Array.isArray(data.sections) ? data.sections : [];
const hasItems = sections.some((section) => (section.items || []).length > 0);
return `
${sections
.map(
(section) => `
<section class="sidebar-section">
<div class="sidebar-section-title">
<span class="section-icon status-${escapeHtmlAttribute(section.status || "draft")}">●</span>
<span data-testid="sidebar-section-title">${escapeHtml(tText(section.title))}</span>
<span class="sidebar-section-count">${escapeHtml(String(section.count || (section.items || []).length))}</span>
</div>
<div class="sidebar-list">
${(section.items || []).map((item) => renderSidebarPostItem(item, view)).join("")}
</div>
</section>
`
)
.join("")}
${hasItems ? "" : renderSidebarEmpty(data.empty_message || "No items")}
`;
}
function renderSidebarPostItem(item, view) {
const tabRef = currentTabRef();
const itemRoute = item.route || view.editor_route;
const tabId = tabIdForItem(item, itemRoute);
const active = tabRef && tabRef.type === itemRoute && tabRef.id === tabId;
const postType = getSidebarPostType(item.categories || []);
const languageBadge = Number(item.language_count) > 1
? `<span class="sidebar-item-language-badge" title="${escapeHtmlAttribute(String(item.language_count))}">${escapeHtml(String(item.language_count))}</span>`
: "";
return `
<button
class="sidebar-item sidebar-post-item post-type-${escapeHtmlAttribute(postType.type)} ${active ? "active" : ""}"
data-open-tab="${escapeHtmlAttribute(tabId)}"
data-open-route="${escapeHtmlAttribute(itemRoute)}"
data-open-title="${escapeHtmlAttribute(item.title || routeLabel(itemRoute))}"
type="button"
>
<span class="post-type-icon" title="${escapeHtmlAttribute(postType.type)}">${escapeHtml(postType.icon)}</span>
<span class="sidebar-item-content">
<span class="sidebar-item-title-row">
<span class="sidebar-item-title">${escapeHtml(item.title || "")}</span>
${languageBadge}
</span>
<span class="sidebar-item-meta">${escapeHtml(formatSidebarAbsoluteDate(item.meta_timestamp))}</span>
</span>
</button>
`;
}
function renderSidebarMediaGrid(data, view) {
const items = Array.isArray(data.items) ? data.items : [];
if (!items.length) {
return renderSidebarEmpty(data.empty_message || "No items");
}
return `
<div class="sidebar-list media-grid">
${items.map((item) => renderSidebarMediaItem(item, view)).join("")}
</div>
`;
}
function renderSidebarItem(item, view) {
function renderSidebarMediaItem(item, view) {
const tabRef = currentTabRef();
const itemRoute = item.route || view.editor_route;
const tabId = tabIdForItem(item, itemRoute);
@@ -170,19 +254,92 @@ function renderSidebarItem(item, view) {
return `
<button
class="sidebar-item ${active ? "active" : ""}"
data-open-tab="${tabId}"
data-open-route="${itemRoute}"
data-open-title="${escapeHtmlAttribute(tText(item.title))}"
class="media-item ${active ? "selected" : ""}"
data-open-tab="${escapeHtmlAttribute(tabId)}"
data-open-route="${escapeHtmlAttribute(itemRoute)}"
data-open-title="${escapeHtmlAttribute(item.title || routeLabel(itemRoute))}"
type="button"
title="${escapeHtmlAttribute(item.title || "") }"
>
<span class="media-thumbnail">${escapeHtml(mediaThumbnailGlyph(item.mime_type))}</span>
<span class="media-item-info">
<span class="media-item-name">${escapeHtml(item.title || "")}</span>
<span class="media-item-size">${escapeHtml(item.meta || "")}</span>
</span>
</button>
`;
}
function renderSidebarEntityList(data, view) {
const items = Array.isArray(data.items) ? data.items : [];
if (!items.length) {
return renderSidebarEmpty(data.empty_message || "No items");
}
return items.map((item) => renderSidebarEntityItem(item, view)).join("");
}
function renderSidebarEntityItem(item, view) {
const tabRef = currentTabRef();
const itemRoute = item.route || view.editor_route;
const tabId = tabIdForItem(item, itemRoute);
const active = tabRef && tabRef.type === itemRoute && tabRef.id === tabId;
const meta = item.updated_at ? formatSidebarRelativeDateMs(item.updated_at) : tText(item.meta || "");
return `
<button
class="chat-list-item ${active ? "active" : ""}"
data-open-tab="${escapeHtmlAttribute(tabId)}"
data-open-route="${escapeHtmlAttribute(itemRoute)}"
data-open-title="${escapeHtmlAttribute(item.title || routeLabel(itemRoute))}"
type="button"
>
<strong>${escapeHtml(tText(item.title))}</strong>
<span>${escapeHtml(tText(item.meta || view.label))}</span>
${item.badge ? `<span class="sidebar-badge">${escapeHtml(tText(item.badge))}</span>` : ""}
<span class="chat-item-content">
<span class="chat-item-title">${escapeHtml(item.title || "")}</span>
<span class="chat-item-date">${escapeHtml(meta || "")}</span>
</span>
</button>
`;
}
function renderSidebarNavList(data, view) {
const items = Array.isArray(data.items) ? data.items : [];
return `
<div class="settings-nav-list">
${items.map((item) => renderSidebarNavItem(item, view)).join("")}
</div>
`;
}
function renderSidebarNavItem(item, view) {
const itemRoute = item.route || view.editor_route;
const tabId = tabIdForItem(item, itemRoute);
const tabTitle = routeLabel(itemRoute);
return `
<button
class="settings-nav-entry"
data-open-tab="${escapeHtmlAttribute(tabId)}"
data-open-route="${escapeHtmlAttribute(itemRoute)}"
data-open-title="${escapeHtmlAttribute(tabTitle)}"
type="button"
>
<span class="settings-nav-entry-icon">${escapeHtml(item.icon || "")}</span>
<span>${escapeHtml(tText(item.title || ""))}</span>
</button>
`;
}
function renderSidebarEmpty(message) {
return `
<div class="sidebar-empty">
<p>${escapeHtml(tText(message))}</p>
</div>
`;
}
function renderTabs() {
const tabs = state.session.tabs;
const node = root.querySelector(".tab-bar");
@@ -361,7 +518,7 @@ function renderDashboard() {
<span class="recent-post-title">${escapeHtml(post.title || "")}</span>
<span class="recent-post-status status-${escapeHtmlAttribute(post.status || "draft")}">${escapeHtml(dashboardStatusLabel(post.status || "draft"))}</span>
<span class="recent-post-date">${escapeHtml(formatDashboardDate(post.updated_at))}</span>
</button>
if (route === "settings" || route === "tags" || route === "style") {
`
)
.join("")}
@@ -1146,6 +1303,7 @@ function closeActiveTab() {
}
const index = state.session.tabs.findIndex((tab) => tab.type === active.type && tab.id === active.id);
if (index < 0) {
return;
}
@@ -1225,8 +1383,16 @@ function activeItem() {
return null;
}
const sections = Object.values(bootstrap.content.sidebar).flatMap((view) => view.sections);
return sections.flatMap((section) => section.items).find((item) => tabIdForItem(item, item.route) === tab.id) || null;
const items = Object.values(bootstrap.content.sidebar).flatMap(flattenSidebarItems);
return items.find((item) => item.route === tab.type && tabIdForItem(item, item.route) === tab.id) || null;
}
function flattenSidebarItems(view) {
if (Array.isArray(view.sections)) {
return view.sections.flatMap((section) => section.items || []);
}
return Array.isArray(view.items) ? view.items : [];
}
function tabMetadata(tab) {
@@ -1442,7 +1608,7 @@ function formatPayloadValue(value) {
}
function tabIdForItem(item, route) {
if (route === "settings" || route === "tags") {
if (route === "settings" || route === "tags" || route === "style") {
return route;
}
@@ -1714,6 +1880,76 @@ function statusLabel(status) {
}
}
function getSidebarPostType(categories) {
const lowerCategories = (categories || []).map((category) => String(category).toLowerCase());
if (lowerCategories.includes("picture") || lowerCategories.includes("photo") || lowerCategories.includes("image")) {
return { icon: "🖼️", type: "picture" };
}
if (lowerCategories.includes("aside") || lowerCategories.includes("note") || lowerCategories.includes("quick")) {
return { icon: "📝", type: "aside" };
}
if (lowerCategories.includes("link") || lowerCategories.includes("bookmark")) {
return { icon: "🔗", type: "link" };
}
if (lowerCategories.includes("video")) {
return { icon: "🎬", type: "video" };
}
if (lowerCategories.includes("quote")) {
return { icon: "💬", type: "quote" };
}
return { icon: "📄", type: "article" };
}
function formatSidebarAbsoluteDate(timestamp) {
if (!timestamp) {
return "";
}
return new Intl.DateTimeFormat(formatLocaleFor(state.uiLanguage), {
month: "short",
day: "numeric",
year: "numeric",
}).format(new Date(timestamp));
}
function formatSidebarRelativeDateMs(timestamp) {
if (!timestamp) {
return "";
}
const date = new Date(timestamp);
const now = new Date();
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return date.toLocaleTimeString(formatLocaleFor(state.uiLanguage), { hour: "numeric", minute: "2-digit" });
}
if (diffDays === 1) {
return t("sidebar.chat.yesterday");
}
if (diffDays < 7) {
return date.toLocaleDateString(formatLocaleFor(state.uiLanguage), { weekday: "short" });
}
return date.toLocaleDateString(formatLocaleFor(state.uiLanguage), { month: "short", day: "numeric" });
}
function mediaThumbnailGlyph(mimeType) {
if (String(mimeType || "").startsWith("image/")) {
return "🖼️";
}
return "📄";
}
function buildDashboardTagCloudItems(items) {
if (!Array.isArray(items) || !items.length) {
return [];

View File

@@ -53,11 +53,14 @@
"sidebar_visible": true,
"sidebar_width": 280,
"active_view": "posts",
"assistant_sidebar_visible": false,
"layout": "post_list",
"sections": [
"assistant_sidebar_width": 360,
"status": "draft",
"count": 1,
"panel": { "visible": false, "active_tab": "tasks" },
"tabs": [],
"active_tab": null,
{ "id": "post-welcome", "title": "Welcome to bDS2", "meta_timestamp": 1774972800000, "language_count": 1, "categories": ["note"], "route": "post" }
"dirty_tabs": []
},
"content": {
@@ -65,98 +68,71 @@
"posts": {
"title": "Posts",
"subtitle": "Drafts and publishing",
"sections": [
{
"title": "Drafts",
"items": [
{ "id": "post-welcome", "title": "Welcome to bDS2", "meta": "Updated today", "badge": "draft", "route": "post" }
]
}
]
"layout": "post_list",
"sections": []
},
"media": {
"title": "Media",
"subtitle": "Images and files",
"sections": [
{
"title": "Media",
"items": [
{ "id": "media-hero", "title": "hero-shot.jpg", "meta": "Image asset", "route": "media" }
]
}
"layout": "media_grid",
"items": [
{ "id": "media-hero", "title": "hero.jpg", "meta": "1.2 MB", "mime_type": "image/jpeg", "route": "media" }
]
},
"scripts": {
"title": "Scripts",
"subtitle": "Automation helpers",
"layout": "entity_list",
"items": [
{ "id": "script-sync", "title": "Sync tags", "updated_at": 1774800000000, "route": "scripts" }
]
},
"templates": {
"title": "Templates",
"subtitle": "Site rendering",
"layout": "entity_list",
"items": [
{ "id": "template-post", "title": "post.liquid", "updated_at": 1774713600000, "route": "templates" }
]
},
"tags": {
"title": "Tags",
"subtitle": "Tag management",
"layout": "nav_list",
"items": [
{ "id": "tags-cloud", "title": "Tag Cloud", "icon": "☁️", "route": "tags" },
{ "id": "tags-manage", "title": "Create / Edit", "icon": "✏️", "route": "tags" },
{ "id": "tags-merge", "title": "Merge Tags", "icon": "🔀", "route": "tags" }
]
},
"chat": {
"title": "Chat",
"subtitle": "AI conversations",
"layout": "entity_list",
"items": [
{ "id": "chat-planning", "title": "Planning session", "updated_at": 1774886400000, "route": "chat" }
]
},
"import": {
"title": "Import",
"subtitle": "Import definitions",
"layout": "entity_list",
"items": []
},
"git": {
"title": "Git",
"subtitle": "Working tree and history",
"layout": "entity_list",
"items": [
{ "id": "git-working-tree", "title": "Working tree", "meta": "Working tree and history", "route": "git_diff" }
]
},
"settings": {
"title": "Settings",
"subtitle": "Project preferences",
"sections": [
{
"title": "Settings",
"items": [
{ "id": "settings", "title": "Project", "meta": "Defaults and paths", "route": "settings" }
]
}
"layout": "nav_list",
"items": [
{ "id": "settings-project", "title": "Project", "icon": "📁", "route": "settings" },
{ "id": "settings-style", "title": "Style", "icon": "🎨", "route": "style" }
]
}
},
"dashboard": {
"title": "dashboard.title",
"subtitle": "dashboard.subtitle",
"post_stats": {
"total_posts": 42,
"draft_count": 18,
"published_count": 21,
"archived_count": 3
},
"media_stats": {
"media_count": 18,
"image_count": 15,
"total_bytes": 12884902
},
"timeline_entries": [
{ "year": 2025, "month": 11, "count": 2 },
{ "year": 2025, "month": 12, "count": 3 },
{ "year": 2026, "month": 1, "count": 5 },
{ "year": 2026, "month": 2, "count": 7 },
{ "year": 2026, "month": 3, "count": 9 },
{ "year": 2026, "month": 4, "count": 6 }
],
"tag_cloud_items": [
{ "tag": "launch", "count": 12, "color": "#2962ff" },
{ "tag": "writing", "count": 7, "color": "#00897b" },
{ "tag": "elixir", "count": 5, "color": "#e65100" }
],
"category_counts": [
{ "category": "notes", "count": 14 },
{ "category": "projects", "count": 8 }
],
"recent_posts": [
{ "id": "post-welcome", "title": "Welcome to bDS2", "status": "draft", "updated_at": 1774972800000 },
{ "id": "post-roadmap", "title": "Roadmap", "status": "published", "updated_at": 1774540800000 }
]
},
"assistant_cards": [
{ "label": "Desktop Runtime", "text": "Static bundle mirrors the desktop shell layout." }
],
"editor_meta": {
"dashboard": [
{ "label": "Status", "value": "Ready" }
]
}
},
"status": {
"left": { "running_task_message": "Static preview", "running_task_overflow": 0 },
"right": {
"post_count": "42 posts",
"media_count": "18 media",
"theme_badge": "desktop-shell",
"offline_mode": true,
"ui_language": "en",
"brand": "bDS"
}
}
}
</script>
<script src="./app.js"></script>
</body>
</html>
},