feat: first take on sidebars
This commit is contained in:
@@ -6,6 +6,7 @@ defmodule BDS.UI.ShellPage do
|
||||
alias BDS.UI.Dashboard
|
||||
alias BDS.UI.MenuBar
|
||||
alias BDS.UI.Registry
|
||||
alias BDS.UI.Sidebar
|
||||
alias BDS.UI.Session
|
||||
alias BDS.UI.Workbench
|
||||
|
||||
@@ -84,7 +85,7 @@ defmodule BDS.UI.ShellPage do
|
||||
session: Session.serialize(workbench),
|
||||
task_status: task_status,
|
||||
content: %{
|
||||
sidebar: sidebar_content(),
|
||||
sidebar: sidebar_content(projects.active_project_id),
|
||||
dashboard: dashboard,
|
||||
assistant_cards: assistant_cards(),
|
||||
editor_meta: editor_meta(task_status)
|
||||
@@ -162,72 +163,15 @@ defmodule BDS.UI.ShellPage do
|
||||
}
|
||||
end
|
||||
|
||||
defp sidebar_content do
|
||||
%{
|
||||
"posts" => %{
|
||||
title: "Posts",
|
||||
subtitle: "Drafts, published entries, and archive history",
|
||||
sections: [
|
||||
%{
|
||||
title: "Drafts",
|
||||
items: [
|
||||
%{id: "post-welcome", title: "Welcome to bDS2", meta: "Updated today", badge: "draft", route: "post"},
|
||||
%{id: "post-launch-plan", title: "Launch plan", meta: "Updated yesterday", badge: "draft", route: "post"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
title: "Published",
|
||||
items: [
|
||||
%{id: "post-roadmap", title: "Roadmap", meta: "Published Feb 10, 2026", badge: "2 langs", route: "post"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
title: "Archived",
|
||||
items: [
|
||||
%{id: "post-retrospective", title: "Retrospective", meta: "Archived Jan 12, 2026", badge: "archive", route: "post"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"pages" => simple_list_view("Pages", "Standalone pages", [
|
||||
%{id: "page-about", title: "About", meta: "Static page", route: "post"},
|
||||
%{id: "page-contact", title: "Contact", meta: "Static page", route: "post"}
|
||||
]),
|
||||
"media" => simple_list_view("Media", "Images and files", [
|
||||
%{id: "media-hero", title: "hero-shot.jpg", meta: "Image asset", route: "media"},
|
||||
%{id: "media-banner", title: "launch-banner.png", meta: "Image asset", route: "media"}
|
||||
]),
|
||||
"scripts" => simple_list_view("Scripts", "Automation helpers", [
|
||||
%{id: "script-import", title: "Import posts", meta: "Lua utility", route: "scripts"},
|
||||
%{id: "script-sync", title: "Sync tags", meta: "Lua utility", route: "scripts"}
|
||||
]),
|
||||
"templates" => simple_list_view("Templates", "Site rendering", [
|
||||
%{id: "template-post", title: "post.liquid", meta: "Post template", route: "templates"},
|
||||
%{id: "template-list", title: "list.liquid", meta: "List template", route: "templates"}
|
||||
]),
|
||||
"tags" => simple_list_view("Tags", "Tag management", [
|
||||
%{id: "tag-launch", title: "launch", meta: "12 posts", route: "tags"},
|
||||
%{id: "tag-writing", title: "writing", meta: "7 posts", route: "tags"}
|
||||
]),
|
||||
"chat" => simple_list_view("Chat", "AI conversations", [
|
||||
%{id: "chat-planning", title: "Planning session", meta: "Offline gated", route: "chat"},
|
||||
%{id: "chat-translation", title: "Translation QA", meta: "Offline gated", route: "chat"}
|
||||
]),
|
||||
"import" => simple_list_view("Import", "Import definitions", [
|
||||
%{id: "import-wordpress", title: "WordPress import", meta: "Ready", route: "import"}
|
||||
]),
|
||||
"git" => simple_list_view("Git", "Working tree and history", [
|
||||
%{id: "git-working-tree", title: "Working tree", meta: "3 changed files", route: "git_diff"}
|
||||
]),
|
||||
"settings" => simple_list_view("Settings", "Project and publishing", [
|
||||
%{id: "settings-project", title: "Project", meta: "Paths and defaults", route: "settings"},
|
||||
%{id: "settings-ai", title: "AI", meta: "Offline controls", route: "settings"}
|
||||
])
|
||||
}
|
||||
defp sidebar_content(project_id) do
|
||||
Sidebar.snapshot(project_id)
|
||||
rescue
|
||||
error in [Exqlite.Error, DBConnection.OwnershipError] ->
|
||||
if match?(%Exqlite.Error{}, error) and not String.contains?(Exception.message(error), "no such table") do
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
|
||||
defp simple_list_view(title, subtitle, items) do
|
||||
%{title: title, subtitle: subtitle, sections: [%{title: title, items: items}]}
|
||||
Sidebar.empty_snapshot()
|
||||
end
|
||||
|
||||
defp dashboard_content(project_id) do
|
||||
|
||||
295
lib/bds/ui/sidebar.ex
Normal file
295
lib/bds/ui/sidebar.ex
Normal file
@@ -0,0 +1,295 @@
|
||||
defmodule BDS.UI.Sidebar do
|
||||
@moduledoc false
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.AI.ChatConversation
|
||||
alias BDS.Media.Media
|
||||
alias BDS.Posts.Post
|
||||
alias BDS.Posts.Translation
|
||||
alias BDS.Repo
|
||||
alias BDS.Scripts.Script
|
||||
alias BDS.Tags.Tag
|
||||
alias BDS.Templates.Template
|
||||
|
||||
@page_category "page"
|
||||
|
||||
def snapshot(nil), do: empty_snapshot()
|
||||
|
||||
def snapshot(project_id) when is_binary(project_id) do
|
||||
posts = list_posts(project_id)
|
||||
translation_counts = translation_counts(project_id)
|
||||
media_items = list_media(project_id)
|
||||
scripts = list_scripts(project_id)
|
||||
templates = list_templates(project_id)
|
||||
tags = list_tags(project_id)
|
||||
conversations = list_conversations()
|
||||
|
||||
%{
|
||||
"posts" => posts_view(posts, translation_counts, false),
|
||||
"pages" => posts_view(posts, translation_counts, true),
|
||||
"media" => media_view(media_items),
|
||||
"scripts" => entity_list_view("Scripts", "Automation helpers", "scripts", scripts),
|
||||
"templates" =>
|
||||
entity_list_view("Templates", "Site rendering", "templates", templates),
|
||||
"tags" => tags_nav_view(tags),
|
||||
"chat" => entity_list_view("Chat", "AI conversations", "chat", conversations),
|
||||
"import" => entity_list_view("Import", "Import definitions", "import", []),
|
||||
"git" => git_view(),
|
||||
"settings" => settings_nav_view()
|
||||
}
|
||||
end
|
||||
|
||||
def empty_snapshot do
|
||||
%{
|
||||
"posts" => posts_view([], %{}, false),
|
||||
"pages" => posts_view([], %{}, true),
|
||||
"media" => media_view([]),
|
||||
"scripts" => entity_list_view("Scripts", "Automation helpers", "scripts", []),
|
||||
"templates" => entity_list_view("Templates", "Site rendering", "templates", []),
|
||||
"tags" => tags_nav_view([]),
|
||||
"chat" => entity_list_view("Chat", "AI conversations", "chat", []),
|
||||
"import" => entity_list_view("Import", "Import definitions", "import", []),
|
||||
"git" => git_view(),
|
||||
"settings" => settings_nav_view()
|
||||
}
|
||||
end
|
||||
|
||||
defp posts_view(posts, translation_counts, pages?) do
|
||||
filtered_posts = Enum.filter(posts, &(page_post?(&1) == pages?))
|
||||
grouped_posts = group_posts(filtered_posts)
|
||||
|
||||
%{
|
||||
title: if(pages?, do: "Pages", else: "Posts"),
|
||||
subtitle: if(pages?, do: "Standalone pages", else: "Drafts, published entries, and archive history"),
|
||||
layout: "post_list",
|
||||
empty_message: if(pages?, do: "No items", else: "No items"),
|
||||
sections: [
|
||||
build_post_section("Drafts", :draft, grouped_posts.draft, translation_counts, false),
|
||||
build_post_section("Published", :published, grouped_posts.published, translation_counts, true),
|
||||
build_post_section("Archived", :archived, grouped_posts.archived, translation_counts, false)
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp media_view(media_items) do
|
||||
%{
|
||||
title: "Media",
|
||||
subtitle: "Images and files",
|
||||
layout: "media_grid",
|
||||
empty_message: "No items",
|
||||
items:
|
||||
Enum.map(media_items, fn media ->
|
||||
%{
|
||||
id: media.id,
|
||||
title: display_media_title(media),
|
||||
meta: media_size_label(media.size),
|
||||
mime_type: media.mime_type,
|
||||
route: "media"
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
defp tags_nav_view(tags) do
|
||||
%{
|
||||
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"}
|
||||
],
|
||||
summary_badge: length(tags)
|
||||
}
|
||||
end
|
||||
|
||||
defp settings_nav_view do
|
||||
%{
|
||||
title: "Settings",
|
||||
subtitle: "Project and publishing",
|
||||
layout: "nav_list",
|
||||
items: [
|
||||
%{id: "settings-project", title: "Project", icon: "📁", route: "settings"},
|
||||
%{id: "settings-editor", title: "Editor", icon: "📝", route: "settings"},
|
||||
%{id: "settings-content", title: "Content", icon: "📋", route: "settings"},
|
||||
%{id: "settings-ai", title: "AI", icon: "🤖", route: "settings"},
|
||||
%{id: "settings-technology", title: "Technology", icon: "⚙️", route: "settings"},
|
||||
%{id: "settings-publishing", title: "Publishing", icon: "🚀", route: "settings"},
|
||||
%{id: "settings-data", title: "Data", icon: "🗄️", route: "settings"},
|
||||
%{id: "settings-mcp", title: "MCP", icon: "🔌", route: "settings"},
|
||||
%{id: "settings-style", title: "Style", icon: "🎨", route: "style"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp git_view do
|
||||
%{
|
||||
title: "Git",
|
||||
subtitle: "Working tree and history",
|
||||
layout: "entity_list",
|
||||
empty_message: "No items",
|
||||
items: [
|
||||
%{id: "git-working-tree", title: "Working tree", meta: "Working tree and history", route: "git_diff", updated_at: nil}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp entity_list_view(title, subtitle, route, items) do
|
||||
%{
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
layout: "entity_list",
|
||||
empty_message: "No items",
|
||||
items:
|
||||
Enum.map(items, fn item ->
|
||||
%{
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
meta: Map.get(item, :meta),
|
||||
updated_at: Map.get(item, :updated_at),
|
||||
route: route
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
defp build_post_section(title, status, posts, translation_counts, published_meta?) do
|
||||
%{
|
||||
id: Atom.to_string(status),
|
||||
title: title,
|
||||
status: Atom.to_string(status),
|
||||
count: length(posts),
|
||||
items:
|
||||
Enum.map(posts, fn post ->
|
||||
%{
|
||||
id: post.id,
|
||||
title: display_post_title(post),
|
||||
categories: post.categories || [],
|
||||
language_count: 1 + Map.get(translation_counts, post.id, 0),
|
||||
meta_timestamp: if(published_meta?, do: post.published_at || post.updated_at, else: post.updated_at),
|
||||
route: "post"
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
defp list_posts(project_id) do
|
||||
Repo.all(
|
||||
from post in Post,
|
||||
where: post.project_id == ^project_id,
|
||||
order_by: [desc: post.updated_at, desc: post.created_at],
|
||||
select: %{
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
status: post.status,
|
||||
categories: post.categories,
|
||||
updated_at: post.updated_at,
|
||||
published_at: post.published_at
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp translation_counts(project_id) do
|
||||
Repo.all(
|
||||
from translation in Translation,
|
||||
where: translation.project_id == ^project_id,
|
||||
group_by: translation.translation_for,
|
||||
select: {translation.translation_for, count(translation.id)}
|
||||
)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp list_media(project_id) do
|
||||
Repo.all(
|
||||
from media in Media,
|
||||
where: media.project_id == ^project_id,
|
||||
order_by: [desc: media.updated_at, desc: media.created_at],
|
||||
select: %{
|
||||
id: media.id,
|
||||
title: media.title,
|
||||
original_name: media.original_name,
|
||||
mime_type: media.mime_type,
|
||||
size: media.size
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp list_scripts(project_id) do
|
||||
Repo.all(
|
||||
from script in Script,
|
||||
where: script.project_id == ^project_id,
|
||||
order_by: [desc: script.updated_at, desc: script.created_at],
|
||||
select: %{id: script.id, title: script.title, updated_at: script.updated_at}
|
||||
)
|
||||
end
|
||||
|
||||
defp list_templates(project_id) do
|
||||
Repo.all(
|
||||
from template in Template,
|
||||
where: template.project_id == ^project_id,
|
||||
order_by: [desc: template.updated_at, desc: template.created_at],
|
||||
select: %{id: template.id, title: template.title, updated_at: template.updated_at}
|
||||
)
|
||||
end
|
||||
|
||||
defp list_tags(project_id) do
|
||||
Repo.all(
|
||||
from tag in Tag,
|
||||
where: tag.project_id == ^project_id,
|
||||
order_by: [asc: tag.name],
|
||||
select: %{id: tag.id, title: tag.name, updated_at: tag.updated_at}
|
||||
)
|
||||
end
|
||||
|
||||
defp list_conversations do
|
||||
Repo.all(
|
||||
from conversation in ChatConversation,
|
||||
order_by: [desc: conversation.updated_at, desc: conversation.created_at],
|
||||
select: %{id: conversation.id, title: conversation.title, updated_at: conversation.updated_at}
|
||||
)
|
||||
end
|
||||
|
||||
defp group_posts(posts) do
|
||||
Enum.reduce(posts, %{draft: [], published: [], archived: []}, fn post, acc ->
|
||||
case post.status do
|
||||
:draft -> %{acc | draft: acc.draft ++ [post]}
|
||||
:published -> %{acc | published: acc.published ++ [post]}
|
||||
:archived -> %{acc | archived: acc.archived ++ [post]}
|
||||
_other -> acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp page_post?(post) do
|
||||
Enum.any?(post.categories || [], &(String.downcase(to_string(&1)) == @page_category))
|
||||
end
|
||||
|
||||
defp display_post_title(post) do
|
||||
cond do
|
||||
present?(post.title) -> post.title
|
||||
present?(post.slug) -> post.slug
|
||||
true -> "Untitled"
|
||||
end
|
||||
end
|
||||
|
||||
defp display_media_title(media) do
|
||||
if present?(media.title), do: media.title, else: media.original_name || ""
|
||||
end
|
||||
|
||||
defp media_size_label(size) when is_integer(size) and size < 1024, do: "#{size} B"
|
||||
|
||||
defp media_size_label(size) when is_integer(size) and size < 1024 * 1024 do
|
||||
:erlang.float_to_binary(size / 1024, decimals: 1) <> " KB"
|
||||
end
|
||||
|
||||
defp media_size_label(size) when is_integer(size) do
|
||||
:erlang.float_to_binary(size / (1024 * 1024), decimals: 1) <> " MB"
|
||||
end
|
||||
|
||||
defp media_size_label(_size), do: "0 B"
|
||||
|
||||
defp present?(value), do: value not in [nil, ""]
|
||||
end
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 d’automatisation",
|
||||
"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 d’import",
|
||||
"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 l’atelier bureau est acheminé via le shell Elixir.",
|
||||
"Desktop workbench shell wired through Elixir": "Shell d’atelier bureau câblé via Elixir",
|
||||
"Diff Reports": "Rapports de diff",
|
||||
|
||||
@@ -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",
|
||||
|
||||
216
priv/ui/app.css
216
priv/ui/app.css
@@ -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;
|
||||
}
|
||||
}
|
||||
264
priv/ui/app.js
264
priv/ui/app.js
@@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
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 [];
|
||||
|
||||
@@ -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",
|
||||
"layout": "media_grid",
|
||||
"items": [
|
||||
{ "id": "media-hero", "title": "hero-shot.jpg", "meta": "Image asset", "route": "media" }
|
||||
{ "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",
|
||||
"layout": "nav_list",
|
||||
"items": [
|
||||
{ "id": "settings", "title": "Project", "meta": "Defaults and paths", "route": "settings" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"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 }
|
||||
{ "id": "settings-project", "title": "Project", "icon": "📁", "route": "settings" },
|
||||
{ "id": "settings-style", "title": "Style", "icon": "🎨", "route": "style" }
|
||||
]
|
||||
},
|
||||
"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>
|
||||
@@ -159,6 +159,27 @@ defmodule BDS.UI.ShellTest do
|
||||
assert css =~ ".recent-posts-list"
|
||||
end
|
||||
|
||||
test "shell bootstrap and static bundle expose the old sidebar view contracts" do
|
||||
html = ShellPage.render()
|
||||
css = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.css")
|
||||
js = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.js")
|
||||
|
||||
assert html =~ ~s("layout":"post_list")
|
||||
assert html =~ ~s("layout":"media_grid")
|
||||
assert html =~ ~s("layout":"entity_list")
|
||||
assert html =~ ~s("layout":"nav_list")
|
||||
|
||||
assert js =~ "renderSidebarPostList"
|
||||
assert js =~ "renderSidebarMediaGrid"
|
||||
assert js =~ "renderSidebarEntityList"
|
||||
assert js =~ "renderSidebarNavList"
|
||||
|
||||
assert css =~ ".sidebar-section-title"
|
||||
assert css =~ ".media-grid"
|
||||
assert css =~ ".chat-list-item"
|
||||
assert css =~ ".settings-nav-entry"
|
||||
end
|
||||
|
||||
test "static shell bundle exists for direct browser inspection" do
|
||||
assert File.exists?("/Users/gb/Projects/bDS2/priv/ui/index.html")
|
||||
assert File.exists?("/Users/gb/Projects/bDS2/priv/ui/app.css")
|
||||
|
||||
Reference in New Issue
Block a user