feat: p hase 3 of tailwind migration

This commit is contained in:
2026-05-04 11:12:17 +02:00
parent b17e9cc3f8
commit 35017f9793
24 changed files with 15752 additions and 425 deletions

View File

@@ -1,3 +1,21 @@
@theme {
--color-shell-bg: #1e1e1e;
--color-sidebar-bg: #252526;
--color-activity-bg: #333333;
--color-panel-bg: #1e1e1e;
--color-tab-active-bg: #1e1e1e;
--color-tab-inactive-bg: #2d2d2d;
--color-focus-border: #007fd4;
--color-input-bg: rgba(255, 255, 255, 0.06);
--color-input-border: rgba(255, 255, 255, 0.12);
--color-status-bg: #007acc;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
--text-shell: 13px;
--spacing-titlebar: 34px;
--spacing-tabbar: 35px;
--spacing-statusbar: 22px;
}
:root { :root {
--accent-color: #007acc; --accent-color: #007acc;
--accent-color-transparent: rgba(0, 122, 204, 0.25); --accent-color-transparent: rgba(0, 122, 204, 0.25);

View File

@@ -0,0 +1,47 @@
@layer components {
.btn-base {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 28px;
padding: 4px 10px;
border: 1px solid transparent;
border-radius: 4px;
font: inherit;
line-height: 1.2;
cursor: pointer;
user-select: none;
}
.btn-theme-primary {
color: var(--vscode-button-foreground, #ffffff);
background: var(--vscode-button-background, var(--vscode-focusBorder));
}
.btn-theme-primary:hover {
background: var(--vscode-button-hoverBackground, #0e639c);
}
.btn-theme-danger {
color: var(--vscode-errorForeground, #f48771);
background: transparent;
border-color: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 45%, transparent);
}
.btn-theme-danger:hover {
background: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 14%, transparent);
}
.panel-entry {
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
background-color: var(--vscode-sideBar-background);
}
.monaco-host {
min-width: 0;
min-height: 0;
overflow: hidden;
}
}

View File

@@ -1,6 +1,10 @@
import { Socket } from "phoenix"; import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view"; import { LiveSocket } from "phoenix_live_view";
import "phoenix_html"; import "phoenix_html";
import { syncTitlebarOverlayInsets } from "./bridges/titlebar_overlay.js";
import { createMenuRuntimeCommandRunner } from "./bridges/menu_runtime.js";
import { createHooks } from "./hooks/index.js";
import { createMonacoServices } from "./monaco/services.js";
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const csrfToken = document const csrfToken = document
@@ -119,46 +123,6 @@ document.addEventListener("DOMContentLoaded", () => {
window.localStorage.setItem(key, String(width)); window.localStorage.setItem(key, String(width));
}; };
const syncTitlebarOverlayInsets = () => {
const rootStyle = document.documentElement.style;
const setInsets = (left, right) => {
rootStyle.setProperty("--bds-titlebar-overlay-left", `${left}px`);
rootStyle.setProperty("--bds-titlebar-overlay-right", `${right}px`);
};
const overlay = navigator.windowControlsOverlay;
if (!overlay) {
setInsets(0, 0);
return () => {};
}
const updateInsets = () => {
if (!overlay.visible) {
setInsets(0, 0);
return;
}
const titlebarRect = overlay.getTitlebarAreaRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || titlebarRect.right;
const leftInset = Math.max(0, Math.round(titlebarRect.left));
const rightInset = Math.max(0, Math.round(viewportWidth - titlebarRect.right));
setInsets(leftInset, rightInset);
};
const onGeometryChange = () => updateInsets();
const onResize = () => updateInsets();
updateInsets();
overlay.addEventListener("geometrychange", onGeometryChange);
window.addEventListener("resize", onResize);
return () => {
overlay.removeEventListener("geometrychange", onGeometryChange);
window.removeEventListener("resize", onResize);
};
};
let monacoLoaderPromise; let monacoLoaderPromise;
let liquidLanguageRegistered = false; let liquidLanguageRegistered = false;
let markdownWithMacrosRegistered = false; let markdownWithMacrosRegistered = false;
@@ -213,61 +177,12 @@ document.addEventListener("DOMContentLoaded", () => {
document.documentElement.style.zoom = String(zoom); document.documentElement.style.zoom = String(zoom);
}; };
const runMenuRuntimeCommand = (action) => { const menuRuntimeCommandRunner = createMenuRuntimeCommandRunner({
const editor = activeMonacoEditor(); activeMonacoEditor,
runMonacoEditorAction,
switch (action) { runDocumentCommand,
case "undo": applyAppZoom
return editor ? runMonacoEditorAction(editor, "undo") : runDocumentCommand("undo"); });
case "redo":
return editor ? runMonacoEditorAction(editor, "redo") : runDocumentCommand("redo");
case "cut":
return editor
? runMonacoEditorAction(editor, "editor.action.clipboardCutAction")
: runDocumentCommand("cut");
case "copy":
return editor
? runMonacoEditorAction(editor, "editor.action.clipboardCopyAction")
: runDocumentCommand("copy");
case "paste":
return editor
? runMonacoEditorAction(editor, "editor.action.clipboardPasteAction")
: runDocumentCommand("paste");
case "delete":
return editor ? runMonacoEditorAction(editor, "deleteLeft") : runDocumentCommand("delete");
case "select_all":
return editor
? runMonacoEditorAction(editor, "editor.action.selectAll")
: runDocumentCommand("selectAll");
case "find":
return editor ? runMonacoEditorAction(editor, "actions.find") : false;
case "replace":
return editor ? runMonacoEditorAction(editor, "editor.action.startFindReplaceAction") : false;
case "reload":
case "force_reload":
window.location.reload();
return true;
case "reset_zoom":
applyAppZoom(1);
return true;
case "zoom_in":
applyAppZoom((window.__bdsAppZoom || 1) + 0.1);
return true;
case "zoom_out":
applyAppZoom((window.__bdsAppZoom || 1) - 0.1);
return true;
case "toggle_full_screen":
if (document.fullscreenElement) {
document.exitFullscreen?.();
} else {
document.documentElement.requestFullscreen?.();
}
return true;
default:
return false;
}
};
const cssVar = (name, fallback) => { const cssVar = (name, fallback) => {
const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim(); const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
@@ -588,6 +503,8 @@ document.addEventListener("DOMContentLoaded", () => {
return monacoLoaderPromise; return monacoLoaderPromise;
}; };
const monacoServices = createMonacoServices({ loadMonaco, ensureMonacoTheme });
const Hooks = { const Hooks = {
AppShell: { AppShell: {
mounted() { mounted() {
@@ -738,7 +655,7 @@ document.addEventListener("DOMContentLoaded", () => {
this.handleEvent("menu-runtime-command", ({ action }) => { this.handleEvent("menu-runtime-command", ({ action }) => {
if (action) { if (action) {
runMenuRuntimeCommand(String(action)); menuRuntimeCommandRunner(String(action));
} }
}); });
@@ -1295,7 +1212,7 @@ document.addEventListener("DOMContentLoaded", () => {
this.editor.focus(); this.editor.focus();
}; };
loadMonaco() monacoServices.loadMonaco()
.then(async (monaco) => { .then(async (monaco) => {
if (!this.host || !this.textarea) { if (!this.host || !this.textarea) {
return; return;
@@ -1303,7 +1220,7 @@ document.addEventListener("DOMContentLoaded", () => {
await this.waitForMonacoVisibleSize(); await this.waitForMonacoVisibleSize();
ensureMonacoTheme(monaco); monacoServices.ensureMonacoTheme(monaco);
this.editor = monaco.editor.create(this.host, { this.editor = monaco.editor.create(this.host, {
value: this.textarea.value || "", value: this.textarea.value || "",
@@ -1360,8 +1277,8 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
loadMonaco().then((monaco) => { monacoServices.loadMonaco().then((monaco) => {
ensureMonacoTheme(monaco); monacoServices.ensureMonacoTheme(monaco);
monaco.editor.setTheme("bds-theme"); monaco.editor.setTheme("bds-theme");
if (this.editor.getModel()?.getLanguageId() !== this.language) { if (this.editor.getModel()?.getLanguageId() !== this.language) {
@@ -1430,13 +1347,13 @@ document.addEventListener("DOMContentLoaded", () => {
this.lastFilePath = this.filePath; this.lastFilePath = this.filePath;
}; };
loadMonaco() monacoServices.loadMonaco()
.then((monaco) => { .then((monaco) => {
if (!this.host) { if (!this.host) {
return; return;
} }
ensureMonacoTheme(monaco); monacoServices.ensureMonacoTheme(monaco);
this.editor = monaco.editor.createDiffEditor(this.host, { this.editor = monaco.editor.createDiffEditor(this.host, {
theme: "bds-theme", theme: "bds-theme",
@@ -1470,8 +1387,8 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
loadMonaco().then((monaco) => { monacoServices.loadMonaco().then((monaco) => {
ensureMonacoTheme(monaco); monacoServices.ensureMonacoTheme(monaco);
monaco.editor.setTheme("bds-theme"); monaco.editor.setTheme("bds-theme");
this.editor.updateOptions({ this.editor.updateOptions({
@@ -1515,7 +1432,7 @@ document.addEventListener("DOMContentLoaded", () => {
const liveSocket = new LiveSocket("/live", Socket, { const liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken }, params: { _csrf_token: csrfToken },
hooks: Hooks, hooks: createHooks(Hooks),
metadata: { metadata: {
keydown: (event) => ({ keydown: (event) => ({
key: event.key, key: event.key,

View File

@@ -0,0 +1,57 @@
export const createMenuRuntimeCommandRunner = ({ activeMonacoEditor, runMonacoEditorAction, runDocumentCommand, applyAppZoom }) => {
return (action) => {
const editor = activeMonacoEditor();
switch (action) {
case "undo":
return editor ? runMonacoEditorAction(editor, "undo") : runDocumentCommand("undo");
case "redo":
return editor ? runMonacoEditorAction(editor, "redo") : runDocumentCommand("redo");
case "cut":
return editor
? runMonacoEditorAction(editor, "editor.action.clipboardCutAction")
: runDocumentCommand("cut");
case "copy":
return editor
? runMonacoEditorAction(editor, "editor.action.clipboardCopyAction")
: runDocumentCommand("copy");
case "paste":
return editor
? runMonacoEditorAction(editor, "editor.action.clipboardPasteAction")
: runDocumentCommand("paste");
case "delete":
return editor ? runMonacoEditorAction(editor, "deleteLeft") : runDocumentCommand("delete");
case "select_all":
return editor
? runMonacoEditorAction(editor, "editor.action.selectAll")
: runDocumentCommand("selectAll");
case "find":
return editor ? runMonacoEditorAction(editor, "actions.find") : false;
case "replace":
return editor ? runMonacoEditorAction(editor, "editor.action.startFindReplaceAction") : false;
case "reload":
case "force_reload":
window.location.reload();
return true;
case "reset_zoom":
applyAppZoom(1);
return true;
case "zoom_in":
applyAppZoom((window.__bdsAppZoom || 1) + 0.1);
return true;
case "zoom_out":
applyAppZoom((window.__bdsAppZoom || 1) - 0.1);
return true;
case "toggle_full_screen":
if (document.fullscreenElement) {
document.exitFullscreen?.();
} else {
document.documentElement.requestFullscreen?.();
}
return true;
default:
return false;
}
};
};

View File

@@ -0,0 +1,39 @@
export const syncTitlebarOverlayInsets = () => {
const rootStyle = document.documentElement.style;
const setInsets = (left, right) => {
rootStyle.setProperty("--bds-titlebar-overlay-left", `${left}px`);
rootStyle.setProperty("--bds-titlebar-overlay-right", `${right}px`);
};
const overlay = navigator.windowControlsOverlay;
if (!overlay) {
setInsets(0, 0);
return () => {};
}
const updateInsets = () => {
if (!overlay.visible) {
setInsets(0, 0);
return;
}
const titlebarRect = overlay.getTitlebarAreaRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || titlebarRect.right;
const leftInset = Math.max(0, Math.round(titlebarRect.left));
const rightInset = Math.max(0, Math.round(viewportWidth - titlebarRect.right));
setInsets(leftInset, rightInset);
};
const onGeometryChange = () => updateInsets();
const onResize = () => updateInsets();
updateInsets();
overlay.addEventListener("geometrychange", onGeometryChange);
window.addEventListener("resize", onResize);
return () => {
overlay.removeEventListener("geometrychange", onGeometryChange);
window.removeEventListener("resize", onResize);
};
};

1
assets/js/hooks/index.js Normal file
View File

@@ -0,0 +1 @@
export const createHooks = (hooks) => hooks;

View File

@@ -0,0 +1 @@
export const createMonacoServices = (services) => services;

View File

@@ -1,6 +1,6 @@
<div id={"chat-editor-#{@chat_editor.id}"} class="chat-panel" data-testid="chat-editor" phx-hook="ChatSurface"> <div id={"chat-editor-#{@chat_editor.id}"} class="chat-panel flex h-full min-h-0 flex-col" data-testid="chat-editor" phx-hook="ChatSurface">
<div class="chat-panel-header"> <div class="chat-panel-header flex shrink-0 items-center justify-between gap-3">
<div class="chat-panel-title"> <div class="chat-panel-title flex min-w-0 flex-1 items-center justify-between gap-3">
<span class="chat-panel-title-main"> <span class="chat-panel-title-main">
<%= if @chat_editor.needs_api_key? do %> <%= if @chat_editor.needs_api_key? do %>
<%= dgettext("ui", "AI Chat Setup") %> <%= dgettext("ui", "AI Chat Setup") %>
@@ -10,9 +10,9 @@
</span> </span>
<%= unless @chat_editor.needs_api_key? do %> <%= unless @chat_editor.needs_api_key? do %>
<span class="chat-model-selector-wrap"> <span class="chat-model-selector-wrap relative shrink-0">
<button <button
class="chat-model-selector-button chat-model-selector-inline" class="chat-model-selector-button chat-model-selector-inline inline-flex items-center gap-2"
type="button" type="button"
phx-click="toggle_chat_model_selector" phx-click="toggle_chat_model_selector"
phx-target={@myself} phx-target={@myself}
@@ -23,7 +23,7 @@
</button> </button>
<%= if @chat_editor.model_selector_open? and @chat_editor.available_models != [] do %> <%= if @chat_editor.model_selector_open? and @chat_editor.available_models != [] do %>
<div class="chat-model-selector-menu"> <div class="chat-model-selector-menu absolute right-0 top-full z-10 mt-2 flex min-w-56 flex-col">
<%= for group <- @chat_editor.available_model_groups do %> <%= for group <- @chat_editor.available_model_groups do %>
<section class="chat-model-provider-group" data-testid="chat-model-provider-group" data-provider={group.provider}> <section class="chat-model-provider-group" data-testid="chat-model-provider-group" data-provider={group.provider}>
<%= if length(@chat_editor.available_model_groups) > 1 do %> <%= if length(@chat_editor.available_model_groups) > 1 do %>
@@ -33,7 +33,7 @@
<%= for model <- group.models do %> <%= for model <- group.models do %>
<button <button
class={[ class={[
"chat-model-selector-option", "chat-model-selector-option flex items-center justify-between gap-2 text-left",
if(model.id == @chat_editor.effective_model, do: "active") if(model.id == @chat_editor.effective_model, do: "active")
]} ]}
type="button" type="button"
@@ -55,9 +55,9 @@
</div> </div>
</div> </div>
<div class="chat-messages chat-surface-scroll"> <div class="chat-messages chat-surface-scroll min-h-0 flex-1 overflow-auto">
<%= if @chat_editor.needs_api_key? do %> <%= if @chat_editor.needs_api_key? do %>
<div class="chat-welcome chat-api-key-state" data-testid="chat-api-key-required"> <div class="chat-welcome chat-api-key-state flex flex-col items-start gap-3" data-testid="chat-api-key-required">
<div class="chat-welcome-icon">🔑</div> <div class="chat-welcome-icon">🔑</div>
<h2><%= dgettext("ui", "API Key Required") %></h2> <h2><%= dgettext("ui", "API Key Required") %></h2>
<p><%= dgettext("ui", "Configure an API key in Settings to enable AI chat.") %></p> <p><%= dgettext("ui", "Configure an API key in Settings to enable AI chat.") %></p>
@@ -67,7 +67,7 @@
</div> </div>
<% else %> <% else %>
<%= if Enum.empty?(@chat_editor.messages) and not @chat_editor.is_streaming do %> <%= if Enum.empty?(@chat_editor.messages) and not @chat_editor.is_streaming do %>
<div class="chat-welcome"> <div class="chat-welcome flex flex-col items-start gap-3">
<div class="chat-welcome-icon">🤖</div> <div class="chat-welcome-icon">🤖</div>
<h2><%= dgettext("ui", "Welcome to the AI Assistant") %></h2> <h2><%= dgettext("ui", "Welcome to the AI Assistant") %></h2>
<p><%= dgettext("ui", "I can help you manage your blog with rich visualizations. Try asking me to:") %></p> <p><%= dgettext("ui", "I can help you manage your blog with rich visualizations. Try asking me to:") %></p>
@@ -81,7 +81,7 @@
</div> </div>
<% else %> <% else %>
<%= if @chat_editor.pending_user_message do %> <%= if @chat_editor.pending_user_message do %>
<div class="chat-message user pending" data-testid="chat-pending-user-message"> <div class="chat-message user pending flex items-start gap-3" data-testid="chat-pending-user-message">
<div class="chat-message-avatar">👤</div> <div class="chat-message-avatar">👤</div>
<div class="chat-message-content"> <div class="chat-message-content">
<div class="chat-message-header"> <div class="chat-message-header">
@@ -93,7 +93,7 @@
<% end %> <% end %>
<%= for message <- @chat_editor.messages do %> <%= for message <- @chat_editor.messages do %>
<div class={["chat-message", to_string(message.role || "assistant")]}> <div class={["chat-message flex items-start gap-3", to_string(message.role || "assistant")]}>
<div class="chat-message-avatar"><%= if message.role == :user, do: "👤", else: "🤖" %></div> <div class="chat-message-avatar"><%= if message.role == :user, do: "👤", else: "🤖" %></div>
<div class="chat-message-content"> <div class="chat-message-content">
<div class="chat-message-header"><span class="chat-message-role"><%= message_role_label(message.role) %></span></div> <div class="chat-message-header"><span class="chat-message-role"><%= message_role_label(message.role) %></span></div>
@@ -113,7 +113,7 @@
<% end %> <% end %>
<%= if @chat_editor.is_streaming and (@chat_editor.streaming_content != "" or @chat_editor.streaming_tool_markers != []) do %> <%= if @chat_editor.is_streaming and (@chat_editor.streaming_content != "" or @chat_editor.streaming_tool_markers != []) do %>
<div class="chat-message assistant streaming" data-testid="chat-streaming-message"> <div class="chat-message assistant streaming flex items-start gap-3" data-testid="chat-streaming-message">
<div class="chat-message-avatar">🤖</div> <div class="chat-message-avatar">🤖</div>
<div class="chat-message-content"> <div class="chat-message-content">
<div class="chat-message-header"> <div class="chat-message-header">
@@ -133,7 +133,7 @@
<% end %> <% end %>
<%= if @chat_editor.is_streaming and @chat_editor.streaming_content == "" and @chat_editor.streaming_tool_markers == [] do %> <%= if @chat_editor.is_streaming and @chat_editor.streaming_content == "" and @chat_editor.streaming_tool_markers == [] do %>
<div class="chat-message assistant thinking" data-testid="chat-streaming-thinking"> <div class="chat-message assistant thinking flex items-start gap-3" data-testid="chat-streaming-thinking">
<div class="chat-message-avatar">🤖</div> <div class="chat-message-avatar">🤖</div>
<div class="chat-message-content"> <div class="chat-message-content">
<div class="chat-thinking-indicator"> <div class="chat-thinking-indicator">
@@ -147,12 +147,12 @@
</div> </div>
<%= unless @chat_editor.needs_api_key? do %> <%= unless @chat_editor.needs_api_key? do %>
<div class="chat-input-container" data-testid="chat-input-container"> <div class="chat-input-container flex shrink-0 flex-col gap-3" data-testid="chat-input-container">
<%= if @chat_editor.is_streaming do %> <%= if @chat_editor.is_streaming do %>
<button class="chat-abort-button" data-testid="chat-abort-button" type="button" phx-click="abort_chat_editor_message" phx-target={@myself}>◼ <%= dgettext("ui", "Stop") %></button> <button class="chat-abort-button" data-testid="chat-abort-button" type="button" phx-click="abort_chat_editor_message" phx-target={@myself}>◼ <%= dgettext("ui", "Stop") %></button>
<% end %> <% end %>
<form class="chat-input-wrapper" phx-change="change_chat_editor_input" phx-submit="send_chat_editor_message" phx-target={@myself}> <form class="chat-input-wrapper flex items-end gap-2" phx-change="change_chat_editor_input" phx-submit="send_chat_editor_message" phx-target={@myself}>
<textarea class="chat-input chat-surface-input" name="message" rows="1" placeholder={dgettext("ui", "Type a message...")} disabled={@chat_editor.is_streaming}><%= @chat_editor.input %></textarea> <textarea class="chat-input chat-surface-input" name="message" rows="1" placeholder={dgettext("ui", "Type a message...")} disabled={@chat_editor.is_streaming}><%= @chat_editor.input %></textarea>
<button class="chat-send-button" data-testid="chat-send-button" type="button" phx-click="send_chat_editor_message" phx-target={@myself} disabled={@chat_editor.send_disabled?}>↑</button> <button class="chat-send-button" data-testid="chat-send-button" type="button" phx-click="send_chat_editor_message" phx-target={@myself} disabled={@chat_editor.send_disabled?}>↑</button>
</form> </form>

View File

@@ -1,5 +1,5 @@
<div <div
class="app" class="app flex h-full w-full flex-col"
id="bds-shell-app" id="bds-shell-app"
phx-hook="AppShell" phx-hook="AppShell"
data-shortcuts={encoded_shortcuts(@client_shortcuts)} data-shortcuts={encoded_shortcuts(@client_shortcuts)}
@@ -112,12 +112,12 @@
</div> </div>
<% end %> <% end %>
<div class="app-main"> <div class="app-main flex min-h-0 flex-1 overflow-hidden">
<aside class="activity-bar" data-region="activity-bar"> <aside class="activity-bar flex h-full shrink-0 flex-col items-center justify-between" data-region="activity-bar">
<div class="activity-bar-top"> <div class="activity-bar-top flex flex-col items-center gap-1">
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :top)) do %> <%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :top)) do %>
<button <button
class={["activity-bar-item", if(button.active, do: "active")]} class={["activity-bar-item inline-flex h-12 w-12 items-center justify-center", if(button.active, do: "active")]}
data-testid="activity-button" data-testid="activity-button"
data-view={button.id} data-view={button.id}
data-active={to_string(button.active)} data-active={to_string(button.active)}
@@ -134,10 +134,10 @@
</button> </button>
<% end %> <% end %>
</div> </div>
<div class="activity-bar-bottom"> <div class="activity-bar-bottom flex flex-col items-center gap-1">
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :bottom)) do %> <%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :bottom)) do %>
<button <button
class={["activity-bar-item", if(button.active, do: "active")]} class={["activity-bar-item inline-flex h-12 w-12 items-center justify-center", if(button.active, do: "active")]}
data-testid="activity-button" data-testid="activity-button"
data-view={button.id} data-view={button.id}
data-active={to_string(button.active)} data-active={to_string(button.active)}
@@ -157,22 +157,22 @@
</aside> </aside>
<section <section
class={["sidebar-shell", if(not @workbench.sidebar_visible, do: "is-hidden")]} class={["sidebar-shell flex min-w-0 shrink-0 overflow-hidden", if(not @workbench.sidebar_visible, do: "is-hidden")]}
data-testid="sidebar-shell" data-testid="sidebar-shell"
style={"width: #{if(@workbench.sidebar_visible, do: @workbench.sidebar_width, else: 0)}px;"} style={"width: #{if(@workbench.sidebar_visible, do: @workbench.sidebar_width, else: 0)}px;"}
> >
<div class="sidebar" data-region="sidebar"> <div class="sidebar flex min-w-0 flex-1 overflow-hidden" data-region="sidebar">
<div id="sidebar-content" class="sidebar-content sidebar-body" phx-hook="SidebarInteractions"> <div id="sidebar-content" class="sidebar-content sidebar-body flex min-h-0 flex-1 flex-col overflow-y-auto" phx-hook="SidebarInteractions">
<div class="sidebar-section"> <div class="sidebar-section">
<% create_action = sidebar_create_action(@workbench.active_view) %> <% create_action = sidebar_create_action(@workbench.active_view) %>
<div class="sidebar-section-header"> <div class="sidebar-section-header flex items-center justify-between gap-2">
<span><%= String.upcase(sidebar_header_label(@sidebar_header)) %></span> <span><%= String.upcase(sidebar_header_label(@sidebar_header)) %></span>
<%= if create_action || ShellSidebarComponents.filters_enabled?(@sidebar_data) do %> <%= if create_action || ShellSidebarComponents.filters_enabled?(@sidebar_data) do %>
<div class="sidebar-actions"> <div class="sidebar-actions flex items-center gap-1">
<%= if ShellSidebarComponents.filters_enabled?(@sidebar_data) do %> <%= if ShellSidebarComponents.filters_enabled?(@sidebar_data) do %>
<button <button
class={[ class={[
"sidebar-action", "sidebar-action inline-flex h-8 w-8 items-center justify-center",
if(ShellSidebarComponents.filters_visible?(@sidebar_data), do: "active") if(ShellSidebarComponents.filters_visible?(@sidebar_data), do: "active")
]} ]}
data-testid="sidebar-filter-toggle" data-testid="sidebar-filter-toggle"
@@ -188,7 +188,7 @@
<% end %> <% end %>
<%= if create_action do %> <%= if create_action do %>
<button <button
class="sidebar-action" class="sidebar-action inline-flex h-8 w-8 items-center justify-center"
data-testid="sidebar-create-action" data-testid="sidebar-create-action"
data-sidebar-action={create_action.kind} data-sidebar-action={create_action.kind}
type="button" type="button"
@@ -212,16 +212,16 @@
<div class="resizable-panel-divider sidebar-divider" data-resize="sidebar" data-role="resize-handle"></div> <div class="resizable-panel-divider sidebar-divider" data-resize="sidebar" data-role="resize-handle"></div>
</section> </section>
<main class="app-content" data-region="content"> <main class="app-content flex min-w-0 flex-1 flex-col overflow-hidden" data-region="content">
<div class="tab-bar" data-region="tab-bar"> <div class="tab-bar flex h-[35px] shrink-0 items-center overflow-hidden" data-region="tab-bar">
<%= if Enum.empty?(@workbench.tabs) do %> <%= if Enum.empty?(@workbench.tabs) do %>
<div class="tab-bar-empty"><%= dgettext("ui", "Dashboard") %></div> <div class="tab-bar-empty flex h-full items-center px-3 text-sm"><%= dgettext("ui", "Dashboard") %></div>
<% else %> <% else %>
<div class="tab-bar-tabs"> <div class="tab-bar-tabs flex min-w-0 flex-1 items-stretch overflow-x-auto">
<%= for tab <- @workbench.tabs do %> <%= for tab <- @workbench.tabs do %>
<div <div
class={[ class={[
"tab", "tab flex min-w-0 max-w-[240px] shrink-0 items-stretch",
if(@workbench.active_tab == {tab.type, tab.id}, do: "active"), if(@workbench.active_tab == {tab.type, tab.id}, do: "active"),
if(tab.is_transient, do: "transient"), if(tab.is_transient, do: "transient"),
if(Workbench.dirty?(@workbench, tab.type, tab.id), do: "dirty") if(Workbench.dirty?(@workbench, tab.type, tab.id), do: "dirty")
@@ -231,21 +231,21 @@
tabindex="0" tabindex="0"
> >
<button <button
class="tab-select" class="tab-select flex min-w-0 flex-1 items-center gap-2 overflow-hidden px-3 text-sm"
type="button" type="button"
phx-click="select_tab" phx-click="select_tab"
phx-value-type={tab.type} phx-value-type={tab.type}
phx-value-id={tab.id} phx-value-id={tab.id}
> >
<span class="tab-icon"><%= raw(ShellData.activity_icon(BDS.Desktop.ShellLive.TabHelpers.tab_icon_id(tab))) %></span> <span class="tab-icon shrink-0"><%= raw(ShellData.activity_icon(BDS.Desktop.ShellLive.TabHelpers.tab_icon_id(tab))) %></span>
<span class="tab-title"><%= BDS.Desktop.ShellLive.TabHelpers.tab_title(tab, @tab_meta) %></span> <span class="tab-title truncate"><%= BDS.Desktop.ShellLive.TabHelpers.tab_title(tab, @tab_meta) %></span>
</button> </button>
<div class="tab-actions"> <div class="tab-actions flex items-center gap-1 pr-2">
<%= if Workbench.dirty?(@workbench, tab.type, tab.id) do %> <%= if Workbench.dirty?(@workbench, tab.type, tab.id) do %>
<span class="tab-dirty-indicator">●</span> <span class="tab-dirty-indicator">●</span>
<% end %> <% end %>
<button <button
class="tab-close" class="tab-close inline-flex h-6 w-6 items-center justify-center"
data-testid="tab-close" data-testid="tab-close"
data-tab-type={tab.type} data-tab-type={tab.type}
data-tab-id={tab.id} data-tab-id={tab.id}
@@ -265,7 +265,7 @@
<% end %> <% end %>
</div> </div>
<section class="editor-shell" data-region="editor"> <section class="editor-shell flex min-h-0 flex-1 flex-col overflow-hidden" data-region="editor">
<%= if is_nil(@current_tab) do %> <%= if is_nil(@current_tab) do %>
<div class="editor-empty"> <div class="editor-empty">
<div class="dashboard-content"> <div class="dashboard-content">
@@ -452,12 +452,12 @@
<% end %> <% end %>
</section> </section>
<section class={["panel-shell", if(not @workbench.panel.visible, do: "is-hidden")]} data-region="panel"> <section class={["panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden", if(not @workbench.panel.visible, do: "is-hidden")]} data-region="panel">
<div class="panel-header"> <div class="panel-header flex items-center justify-between gap-2">
<div class="panel-tabs"> <div class="panel-tabs flex min-w-0 items-center overflow-x-auto">
<%= for tab <- @panel_tabs do %> <%= for tab <- @panel_tabs do %>
<button <button
class={["panel-tab", if(@workbench.panel.active_tab == tab, do: "active")]} class={["panel-tab inline-flex h-9 items-center px-3 text-xs uppercase tracking-wide", if(@workbench.panel.active_tab == tab, do: "active")]}
type="button" type="button"
phx-click="select_panel_tab" phx-click="select_panel_tab"
phx-value-tab={tab} phx-value-tab={tab}
@@ -467,7 +467,7 @@
<% end %> <% end %>
</div> </div>
<button <button
class="panel-close" class="panel-close inline-flex h-8 w-8 items-center justify-center"
data-testid="panel-close" data-testid="panel-close"
type="button" type="button"
phx-click="toggle_panel" phx-click="toggle_panel"
@@ -477,21 +477,21 @@
× ×
</button> </button>
</div> </div>
<div class="panel-content"> <div class="panel-content min-h-0 flex-1 overflow-auto">
<%= BDS.Desktop.ShellLive.PanelRenderer.render_panel_body(assigns) %> <%= BDS.Desktop.ShellLive.PanelRenderer.render_panel_body(assigns) %>
</div> </div>
</section> </section>
</main> </main>
<section <section
class={["assistant-sidebar-shell", if(not @workbench.assistant_sidebar_visible, do: "is-hidden")]} class={["assistant-sidebar-shell flex min-w-0 shrink-0 overflow-hidden", if(not @workbench.assistant_sidebar_visible, do: "is-hidden")]}
data-testid="assistant-shell" data-testid="assistant-shell"
style={"width: #{if(@workbench.assistant_sidebar_visible, do: @workbench.assistant_sidebar_width, else: 0)}px;"} style={"width: #{if(@workbench.assistant_sidebar_visible, do: @workbench.assistant_sidebar_width, else: 0)}px;"}
> >
<div class="resizable-panel-divider assistant-divider" data-resize="assistant" data-role="resize-handle"></div> <div class="resizable-panel-divider assistant-divider" data-resize="assistant" data-role="resize-handle"></div>
<aside class="assistant-sidebar" data-region="assistant-sidebar"> <aside class="assistant-sidebar flex min-w-0 flex-1 overflow-hidden" data-region="assistant-sidebar">
<div class="assistant-content"> <div class="assistant-content flex min-h-0 flex-1 flex-col">
<header class="assistant-sidebar-header"> <header class="assistant-sidebar-header flex items-start justify-between gap-3">
<div class="assistant-sidebar-heading"> <div class="assistant-sidebar-heading">
<strong><%= dgettext("ui", "AI Assistant") %></strong> <strong><%= dgettext("ui", "AI Assistant") %></strong>
<span class="assistant-sidebar-description"><%= dgettext("ui", "AI conversations") %></span> <span class="assistant-sidebar-description"><%= dgettext("ui", "AI conversations") %></span>
@@ -504,12 +504,12 @@
</span> </span>
</header> </header>
<section class="assistant-sidebar-context" data-testid="assistant-context"> <section class="assistant-sidebar-context flex shrink-0 flex-col gap-2" data-testid="assistant-context">
<div class="assistant-sidebar-context-row"> <div class="assistant-sidebar-context-row flex items-center justify-between gap-2">
<span class="assistant-sidebar-context-label"><%= dgettext("ui", "Project") %></span> <span class="assistant-sidebar-context-label"><%= dgettext("ui", "Project") %></span>
<span class="assistant-sidebar-context-value"><%= BDS.Desktop.ShellLive.ChatSurface.assistant_project_name(@current_project) %></span> <span class="assistant-sidebar-context-value"><%= BDS.Desktop.ShellLive.ChatSurface.assistant_project_name(@current_project) %></span>
</div> </div>
<div class="assistant-sidebar-context-row"> <div class="assistant-sidebar-context-row flex items-center justify-between gap-2">
<span class="assistant-sidebar-context-label"><%= dgettext("ui", "Editor") %></span> <span class="assistant-sidebar-context-label"><%= dgettext("ui", "Editor") %></span>
<span class="assistant-sidebar-context-value"><%= BDS.Desktop.ShellLive.TabHelpers.tab_title(@current_tab, @tab_meta) %></span> <span class="assistant-sidebar-context-value"><%= BDS.Desktop.ShellLive.TabHelpers.tab_title(@current_tab, @tab_meta) %></span>
</div> </div>
@@ -517,13 +517,13 @@
</section> </section>
<form <form
class="assistant-sidebar-prompt-form" class="assistant-sidebar-prompt-form flex shrink-0 flex-col gap-3"
data-testid="assistant-prompt-form" data-testid="assistant-prompt-form"
phx-change="update_assistant_prompt" phx-change="update_assistant_prompt"
phx-submit="submit_assistant_prompt" phx-submit="submit_assistant_prompt"
> >
<textarea <textarea
class="assistant-sidebar-prompt" class="assistant-sidebar-prompt min-h-[8rem] w-full resize-y"
data-testid="assistant-prompt-input" data-testid="assistant-prompt-input"
name="assistant[prompt]" name="assistant[prompt]"
rows="6" rows="6"
@@ -541,19 +541,19 @@
</form> </form>
<%= if Enum.empty?(@assistant_messages) do %> <%= if Enum.empty?(@assistant_messages) do %>
<div class="assistant-sidebar-welcome"> <div class="assistant-sidebar-welcome min-h-0 flex-1 overflow-auto">
<%= for card <- @assistant_cards do %> <%= for card <- @assistant_cards do %>
<section class="assistant-card"> <section class="assistant-card flex flex-col gap-1">
<strong><%= card.label %></strong> <strong><%= card.label %></strong>
<span><%= card.text %></span> <span><%= card.text %></span>
</section> </section>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>
<div class="assistant-sidebar-transcript"> <div class="assistant-sidebar-transcript min-h-0 flex-1 overflow-auto">
<%= for message <- @assistant_messages do %> <%= for message <- @assistant_messages do %>
<article <article
class={["assistant-sidebar-message", message.role]} class={["assistant-sidebar-message flex flex-col gap-1", message.role]}
data-testid={BDS.Desktop.ShellLive.ChatSurface.assistant_message_testid(message.role)} data-testid={BDS.Desktop.ShellLive.ChatSurface.assistant_message_testid(message.role)}
> >
<span class="assistant-sidebar-message-role"><%= BDS.Desktop.ShellLive.ChatSurface.assistant_message_label(message.role) %></span> <span class="assistant-sidebar-message-role"><%= BDS.Desktop.ShellLive.ChatSurface.assistant_message_label(message.role) %></span>
@@ -567,12 +567,12 @@
</section> </section>
</div> </div>
<footer class="status-bar" data-region="status-bar" data-testid="status-bar"> <footer class="status-bar flex h-[22px] shrink-0 items-center justify-between gap-2" data-region="status-bar" data-testid="status-bar">
<div class="status-bar-left"> <div class="status-bar-left flex min-w-0 items-center gap-2 overflow-hidden">
<%= if @is_mac_ui do %> <%= if @is_mac_ui do %>
<div class="status-shell-controls" data-testid="status-shell-controls"> <div class="status-shell-controls flex items-center gap-1" data-testid="status-shell-controls">
<button <button
class="status-shell-toggle-button" class="status-shell-toggle-button inline-flex h-6 w-6 items-center justify-center"
data-testid="toggle-sidebar" data-testid="toggle-sidebar"
type="button" type="button"
phx-click="toggle_sidebar" phx-click="toggle_sidebar"
@@ -584,7 +584,7 @@
</span> </span>
</button> </button>
<button <button
class="status-shell-toggle-button" class="status-shell-toggle-button inline-flex h-6 w-6 items-center justify-center"
data-testid="toggle-panel" data-testid="toggle-panel"
type="button" type="button"
phx-click="toggle_panel" phx-click="toggle_panel"
@@ -596,7 +596,7 @@
</span> </span>
</button> </button>
<button <button
class="status-shell-toggle-button" class="status-shell-toggle-button inline-flex h-6 w-6 items-center justify-center"
data-testid="toggle-assistant" data-testid="toggle-assistant"
type="button" type="button"
phx-click="toggle_assistant_sidebar" phx-click="toggle_assistant_sidebar"
@@ -609,9 +609,9 @@
</button> </button>
</div> </div>
<% end %> <% end %>
<div class="project-selector"> <div class="project-selector relative shrink-0">
<button <button
class="project-selector-trigger" class="project-selector-trigger inline-flex items-center gap-2"
data-testid="project-selector-trigger" data-testid="project-selector-trigger"
type="button" type="button"
title={dgettext("ui", "Switch project")} title={dgettext("ui", "Switch project")}
@@ -659,7 +659,7 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<button class="status-bar-item status-bar-task-button" data-testid="status-task-button" type="button" phx-click="open_tasks_panel"> <button class="status-bar-item status-bar-task-button inline-flex items-center gap-2" data-testid="status-task-button" type="button" phx-click="open_tasks_panel">
<%= if @status.left.running_task_message do %> <%= if @status.left.running_task_message do %>
<span class="task-spinner"></span> <span class="task-spinner"></span>
<% end %> <% end %>
@@ -669,12 +669,12 @@
<% end %> <% end %>
</button> </button>
</div> </div>
<div class="status-bar-right"> <div class="status-bar-right flex items-center gap-2 overflow-hidden">
<span class="status-bar-item"><%= @status.right.post_count %></span> <span class="status-bar-item"><%= @status.right.post_count %></span>
<span class="status-bar-item"><%= @status.right.media_count %></span> <span class="status-bar-item"><%= @status.right.media_count %></span>
<span class="status-bar-item theme-badge"><%= @status.right.theme_badge %></span> <span class="status-bar-item theme-badge"><%= @status.right.theme_badge %></span>
<button class={["status-bar-item", "offline-badge", if(@status.right.offline_mode, do: "active")]} data-testid="status-offline-button" type="button" phx-click="toggle_offline_mode" title={dgettext("ui", "Toggle offline mode")}>✈</button> <button class={["status-bar-item", "offline-badge", if(@status.right.offline_mode, do: "active")]} data-testid="status-offline-button" type="button" phx-click="toggle_offline_mode" title={dgettext("ui", "Toggle offline mode")}>✈</button>
<form class="status-bar-item language-badge" data-testid="status-language-form" phx-change="change_ui_language"> <form class="status-bar-item language-badge flex items-center gap-1" data-testid="status-language-form" phx-change="change_ui_language">
<span><%= dgettext("ui", "UI") %></span> <span><%= dgettext("ui", "UI") %></span>
<select class="status-bar-language-select" name="ui_language" data-testid="status-language-select"> <select class="status-bar-language-select" name="ui_language" data-testid="status-language-select">
<%= for language <- @supported_ui_languages do %> <%= for language <- @supported_ui_languages do %>

View File

@@ -1,26 +1,25 @@
<div class="media-editor editor" data-testid="media-editor"> <div class="media-editor editor flex h-full min-h-0 flex-col" data-testid="media-editor">
<div class="editor-header"> <div class="editor-header flex shrink-0 items-start justify-between gap-3">
<div class="editor-tabs"> <div class="editor-tabs flex min-w-0 flex-1 overflow-hidden">
<div class={[ <div class={[
"editor-tab", "editor-tab active inline-flex max-w-full items-center gap-2 overflow-hidden px-3 py-2",
"active",
if(@media_editor.dirty?, do: "dirty") if(@media_editor.dirty?, do: "dirty")
]}> ]}>
<span class="editor-tab-title" data-testid="editor-title"><%= @media_editor.display_title %></span> <span class="editor-tab-title truncate" data-testid="editor-title"><%= @media_editor.display_title %></span>
<%= if @media_editor.dirty? do %> <%= if @media_editor.dirty? do %>
<span class="editor-tab-dirty" title={dgettext("ui", "Unsaved")}>●</span> <span class="editor-tab-dirty" title={dgettext("ui", "Unsaved")}>●</span>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="editor-actions"> <div class="editor-actions flex flex-wrap items-center justify-end gap-2">
<%= if @media_editor.save_state in [:dirty, :saved] do %> <%= if @media_editor.save_state in [:dirty, :saved] do %>
<span class="auto-save-indicator"><%= media_editor_save_state_label(@media_editor.save_state) %></span> <span class="auto-save-indicator"><%= media_editor_save_state_label(@media_editor.save_state) %></span>
<% end %> <% end %>
<div class="quick-actions-wrapper"> <div class="quick-actions-wrapper relative">
<button <button
class="secondary quick-actions-btn" class="secondary quick-actions-btn inline-flex items-center gap-2"
type="button" type="button"
phx-click="toggle_media_editor_quick_actions" phx-click="toggle_media_editor_quick_actions"
phx-target={@myself} phx-target={@myself}
@@ -30,16 +29,16 @@
</button> </button>
<%= if @media_editor.quick_actions_open? do %> <%= if @media_editor.quick_actions_open? do %>
<div class="quick-actions-menu"> <div class="quick-actions-menu absolute right-0 top-full z-10 mt-2 flex min-w-72 flex-col">
<%= if @media_editor.is_image do %> <%= if @media_editor.is_image do %>
<button <button
class="quick-action-item" class="quick-action-item flex items-start gap-3 text-left"
data-testid="editor-toolbar-overlay-button" data-testid="editor-toolbar-overlay-button"
type="button" type="button"
phx-click="open_overlay" phx-click="open_overlay"
phx-value-kind="ai_suggestions" phx-value-kind="ai_suggestions"
> >
<span class="quick-action-text"> <span class="quick-action-text flex min-w-0 flex-1 flex-col">
<strong><%= dgettext("ui", "AI Suggestions") %></strong> <strong><%= dgettext("ui", "AI Suggestions") %></strong>
<small><%= dgettext("ui", "Review title, alt text, and caption suggestions") %></small> <small><%= dgettext("ui", "Review title, alt text, and caption suggestions") %></small>
</span> </span>
@@ -50,13 +49,13 @@
<% end %> <% end %>
<button <button
class="quick-action-item" class="quick-action-item flex items-start gap-3 text-left"
type="button" type="button"
phx-click="detect_media_editor_language" phx-click="detect_media_editor_language"
phx-target={@myself} phx-target={@myself}
disabled={not @media_editor.can_detect_language?} disabled={not @media_editor.can_detect_language?}
> >
<span class="quick-action-text"> <span class="quick-action-text flex min-w-0 flex-1 flex-col">
<strong><%= dgettext("ui", "Detect Language") %></strong> <strong><%= dgettext("ui", "Detect Language") %></strong>
<small><%= dgettext("ui", "Persist the detected language for this media item") %></small> <small><%= dgettext("ui", "Persist the detected language for this media item") %></small>
</span> </span>
@@ -66,14 +65,14 @@
<div class="quick-actions-divider"></div> <div class="quick-actions-divider"></div>
<button <button
class="quick-action-item" class="quick-action-item flex items-start gap-3 text-left"
data-testid="editor-toolbar-overlay-button" data-testid="editor-toolbar-overlay-button"
type="button" type="button"
phx-click="open_overlay" phx-click="open_overlay"
phx-value-kind="language_picker" phx-value-kind="language_picker"
disabled={not @media_editor.can_translate?} disabled={not @media_editor.can_translate?}
> >
<span class="quick-action-text"> <span class="quick-action-text flex min-w-0 flex-1 flex-col">
<strong><%= dgettext("ui", "Translate") %></strong> <strong><%= dgettext("ui", "Translate") %></strong>
<small><%= dgettext("ui", "Select a target language for this media item") %></small> <small><%= dgettext("ui", "Select a target language for this media item") %></small>
</span> </span>
@@ -101,14 +100,14 @@
</div> </div>
</div> </div>
<div class="editor-content media-editor"> <div class="editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]">
<div class="media-preview"> <div class="media-preview flex min-h-[16rem] items-center justify-center">
<%= if @media_editor.is_image and @media_editor.preview_url do %> <%= if @media_editor.is_image and @media_editor.preview_url do %>
<div class="media-preview-image"> <div class="media-preview-image">
<img src={@media_editor.preview_url} alt={@media_editor.form["alt"] || @media_editor.original_name} /> <img src={@media_editor.preview_url} alt={@media_editor.form["alt"] || @media_editor.original_name} />
</div> </div>
<% else %> <% else %>
<div class="media-preview-placeholder"> <div class="media-preview-placeholder flex h-full w-full flex-col items-center justify-center gap-3">
<svg width="64" height="64" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> <svg width="64" height="64" viewBox="0 0 24 24" fill="currentColor" opacity="0.3">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"></path> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"></path>
</svg> </svg>
@@ -117,58 +116,58 @@
<% end %> <% end %>
</div> </div>
<div class="media-details"> <div class="media-details min-w-0">
<form class="media-editor-details-form" data-testid="media-editor-form" phx-change="change_media_editor" phx-target={@myself}> <form class="media-editor-details-form flex flex-col gap-4" data-testid="media-editor-form" phx-change="change_media_editor" phx-target={@myself}>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "File Name") %></label> <label><%= dgettext("ui", "File Name") %></label>
<input class="post-editor-input disabled" type="text" value={@media_editor.original_name} disabled /> <input class="post-editor-input disabled" type="text" value={@media_editor.original_name} disabled />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "MIME Type") %></label> <label><%= dgettext("ui", "MIME Type") %></label>
<input class="post-editor-input disabled" type="text" value={@media_editor.mime_type} disabled /> <input class="post-editor-input disabled" type="text" value={@media_editor.mime_type} disabled />
</div> </div>
<div class="editor-field-row"> <div class="editor-field-row grid gap-4 md:grid-cols-2">
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Size") %></label> <label><%= dgettext("ui", "Size") %></label>
<input class="post-editor-input disabled" type="text" value={@media_editor.file_size} disabled /> <input class="post-editor-input disabled" type="text" value={@media_editor.file_size} disabled />
</div> </div>
<%= if @media_editor.dimensions do %> <%= if @media_editor.dimensions do %>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Dimensions") %></label> <label><%= dgettext("ui", "Dimensions") %></label>
<input class="post-editor-input disabled" type="text" value={@media_editor.dimensions} disabled /> <input class="post-editor-input disabled" type="text" value={@media_editor.dimensions} disabled />
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Title") %></label> <label><%= dgettext("ui", "Title") %></label>
<input class="post-editor-input" type="text" name="media_editor[title]" value={@media_editor.form["title"]} /> <input class="post-editor-input" type="text" name="media_editor[title]" value={@media_editor.form["title"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Alt Text") %></label> <label><%= dgettext("ui", "Alt Text") %></label>
<input class="post-editor-input" type="text" name="media_editor[alt]" value={@media_editor.form["alt"]} /> <input class="post-editor-input" type="text" name="media_editor[alt]" value={@media_editor.form["alt"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Caption") %></label> <label><%= dgettext("ui", "Caption") %></label>
<textarea class="post-editor-textarea" name="media_editor[caption]" rows="3"><%= @media_editor.form["caption"] %></textarea> <textarea class="post-editor-textarea" name="media_editor[caption]" rows="3"><%= @media_editor.form["caption"] %></textarea>
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Tags") %></label> <label><%= dgettext("ui", "Tags") %></label>
<input class="post-editor-input" type="text" name="media_editor[tags]" value={@media_editor.form["tags"]} /> <input class="post-editor-input" type="text" name="media_editor[tags]" value={@media_editor.form["tags"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Author") %></label> <label><%= dgettext("ui", "Author") %></label>
<input class="post-editor-input" type="text" name="media_editor[author]" value={@media_editor.form["author"]} /> <input class="post-editor-input" type="text" name="media_editor[author]" value={@media_editor.form["author"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Language") %></label> <label><%= dgettext("ui", "Language") %></label>
<select class="post-editor-input" name="media_editor[language]"> <select class="post-editor-input" name="media_editor[language]">
<option value=""><%= dgettext("ui", "None") %></option> <option value=""><%= dgettext("ui", "None") %></option>
@@ -180,15 +179,15 @@
</form> </form>
<%= if @media_editor.form["language"] not in [nil, ""] do %> <%= if @media_editor.form["language"] not in [nil, ""] do %>
<div class="editor-field media-translations-section"> <div class="editor-field media-translations-section flex flex-col gap-2">
<label><%= dgettext("ui", "Translations") %></label> <label><%= dgettext("ui", "Translations") %></label>
<%= if Enum.empty?(@media_editor.translations) do %> <%= if Enum.empty?(@media_editor.translations) do %>
<div class="no-linked-posts"><%= dgettext("ui", "No translations") %></div> <div class="no-linked-posts"><%= dgettext("ui", "No translations") %></div>
<% else %> <% else %>
<div class="linked-posts-list"> <div class="linked-posts-list flex flex-col gap-2">
<%= for translation <- @media_editor.translations do %> <%= for translation <- @media_editor.translations do %>
<div class="linked-post-item"> <div class="linked-post-item flex items-center justify-between gap-2">
<button <button
class="linked-post-title linked-post-link" class="linked-post-title linked-post-link"
type="button" type="button"
@@ -209,7 +208,7 @@
</div> </div>
<% end %> <% end %>
<div class="editor-field linked-posts-section"> <div class="editor-field linked-posts-section flex flex-col gap-2">
<label> <label>
<%= dgettext("ui", "Linked Posts") %> <%= dgettext("ui", "Linked Posts") %>
<button class="add-link-btn" type="button" phx-click="toggle_media_post_picker" phx-target={@myself}> <button class="add-link-btn" type="button" phx-click="toggle_media_post_picker" phx-target={@myself}>
@@ -218,7 +217,7 @@
</label> </label>
<%= if @media_editor.post_picker_open? do %> <%= if @media_editor.post_picker_open? do %>
<div class="post-picker"> <div class="post-picker flex flex-col gap-3">
<div class="post-picker-search"> <div class="post-picker-search">
<input <input
type="text" type="text"
@@ -233,7 +232,7 @@
<%= if Enum.empty?(@media_editor.post_picker_results) do %> <%= if Enum.empty?(@media_editor.post_picker_results) do %>
<div class="no-posts"><%= dgettext("ui", "No posts to link") %></div> <div class="no-posts"><%= dgettext("ui", "No posts to link") %></div>
<% else %> <% else %>
<div class="post-picker-list"> <div class="post-picker-list flex flex-col gap-2">
<%= for result <- @media_editor.post_picker_results do %> <%= for result <- @media_editor.post_picker_results do %>
<button class="post-picker-item" type="button" phx-click="link_media_to_post" phx-target={@myself} phx-value-post-id={result.post_id}> <button class="post-picker-item" type="button" phx-click="link_media_to_post" phx-target={@myself} phx-value-post-id={result.post_id}>
<%= result.title %> <%= result.title %>
@@ -250,9 +249,9 @@
<%= if Enum.empty?(@media_editor.linked_posts) do %> <%= if Enum.empty?(@media_editor.linked_posts) do %>
<div class="no-linked-posts"><%= dgettext("ui", "Not linked to any posts") %></div> <div class="no-linked-posts"><%= dgettext("ui", "Not linked to any posts") %></div>
<% else %> <% else %>
<div class="linked-posts-list"> <div class="linked-posts-list flex flex-col gap-2">
<%= for linked_post <- @media_editor.linked_posts do %> <%= for linked_post <- @media_editor.linked_posts do %>
<div class="linked-post-item"> <div class="linked-post-item flex items-center justify-between gap-2">
<button <button
class="linked-post-title linked-post-link" class="linked-post-title linked-post-link"
type="button" type="button"
@@ -275,27 +274,27 @@
<%= if @media_editor.editing_translation do %> <%= if @media_editor.editing_translation do %>
<div class="translation-modal-backdrop"> <div class="translation-modal-backdrop">
<div class="translation-modal"> <div class="translation-modal flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden">
<div class="translation-modal-header"> <div class="translation-modal-header flex items-center justify-between gap-3">
<h2><%= dgettext("ui", "Edit Translation") %></h2> <h2><%= dgettext("ui", "Edit Translation") %></h2>
<button class="translation-modal-close" type="button" phx-click="close_media_translation_editor" phx-target={@myself}>×</button> <button class="translation-modal-close" type="button" phx-click="close_media_translation_editor" phx-target={@myself}>×</button>
</div> </div>
<form class="translation-modal-body" phx-change="change_media_translation" phx-target={@myself}> <form class="translation-modal-body flex flex-col gap-4 overflow-auto" phx-change="change_media_translation" phx-target={@myself}>
<input type="hidden" name="media_translation[language]" value={@media_editor.editing_translation["language"]} /> <input type="hidden" name="media_translation[language]" value={@media_editor.editing_translation["language"]} />
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Title") %></label> <label><%= dgettext("ui", "Title") %></label>
<input class="post-editor-input" type="text" name="media_translation[title]" value={@media_editor.editing_translation["title"]} /> <input class="post-editor-input" type="text" name="media_translation[title]" value={@media_editor.editing_translation["title"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Alt Text") %></label> <label><%= dgettext("ui", "Alt Text") %></label>
<input class="post-editor-input" type="text" name="media_translation[alt]" value={@media_editor.editing_translation["alt"]} /> <input class="post-editor-input" type="text" name="media_translation[alt]" value={@media_editor.editing_translation["alt"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Caption") %></label> <label><%= dgettext("ui", "Caption") %></label>
<textarea class="post-editor-textarea" name="media_translation[caption]" rows="3"><%= @media_editor.editing_translation["caption"] %></textarea> <textarea class="post-editor-textarea" name="media_translation[caption]" rows="3"><%= @media_editor.editing_translation["caption"] %></textarea>
</div> </div>
</form> </form>
<div class="translation-modal-footer"> <div class="translation-modal-footer flex items-center justify-end gap-2">
<button class="secondary" type="button" phx-click="close_media_translation_editor" phx-target={@myself}><%= dgettext("ui", "Cancel") %></button> <button class="secondary" type="button" phx-click="close_media_translation_editor" phx-target={@myself}><%= dgettext("ui", "Cancel") %></button>
<button type="button" phx-click="save_media_translation" phx-target={@myself}><%= dgettext("ui", "Save") %></button> <button type="button" phx-click="save_media_translation" phx-target={@myself}><%= dgettext("ui", "Save") %></button>
</div> </div>

View File

@@ -1,51 +1,51 @@
<div class="menu-editor-view" data-testid="menu-editor" phx-window-keydown={if(@menu_editor.draft, do: "menu_editor_keydown")} phx-target={@myself}> <div class="menu-editor-view flex h-full min-h-0 flex-col" data-testid="menu-editor" phx-window-keydown={if(@menu_editor.draft, do: "menu_editor_keydown")} phx-target={@myself}>
<div class="menu-editor-header"> <div class="menu-editor-header flex shrink-0 items-start justify-between gap-3">
<div> <div>
<h2><%= @menu_editor.title %></h2> <h2><%= @menu_editor.title %></h2>
<p><%= @menu_editor.description %></p> <p><%= @menu_editor.description %></p>
</div> </div>
</div> </div>
<div class="menu-editor-main"> <div class="menu-editor-main min-h-0 flex-1 overflow-hidden">
<div class="menu-editor-tree-wrap"> <div class="menu-editor-tree-wrap flex h-full min-h-0 flex-col">
<div class="menu-editor-toolbar" data-testid="menu-editor-toolbar" role="toolbar" aria-label={@menu_editor.title}> <div class="menu-editor-toolbar flex flex-wrap items-center gap-2" data-testid="menu-editor-toolbar" role="toolbar" aria-label={@menu_editor.title}>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="add-entry" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="add-entry" phx-target={@myself} title={dgettext("ui", "menuEditor.addEntry")}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="add-entry" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="add-entry" phx-target={@myself} title={dgettext("ui", "menuEditor.addEntry")}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M7 2h2v5h5v2H9v5H7V9H2V7h5V2z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M7 2h2v5h5v2H9v5H7V9H2V7h5V2z" /></svg>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="save" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="save" phx-target={@myself} title={dgettext("ui", "menuEditor.save")}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="save" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="save" phx-target={@myself} title={dgettext("ui", "menuEditor.save")}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2h9l3 3v9H2V2zm2 1v3h6V3H4zm0 9h8V7H4v5z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2h9l3 3v9H2V2zm2 1v3h6V3H4zm0 9h8V7H4v5z" /></svg>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="add-category-archive" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="add-category-archive" phx-target={@myself} title={dgettext("ui", "menuEditor.addCategoryArchive")}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="add-category-archive" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="add-category-archive" phx-target={@myself} title={dgettext("ui", "menuEditor.addCategoryArchive")}>
<span aria-hidden="true"><%= dgettext("ui", "menuEditor.addCategoryArchiveShort") %></span> <span aria-hidden="true"><%= dgettext("ui", "menuEditor.addCategoryArchiveShort") %></span>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="move-up" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="move-up" phx-target={@myself} title={dgettext("ui", "menuEditor.moveUp")} disabled={not @menu_editor.can_move_up?}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="move-up" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="move-up" phx-target={@myself} title={dgettext("ui", "menuEditor.moveUp")} disabled={not @menu_editor.can_move_up?}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 3l4 4H9v6H7V7H4l4-4z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 3l4 4H9v6H7V7H4l4-4z" /></svg>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="move-down" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="move-down" phx-target={@myself} title={dgettext("ui", "menuEditor.moveDown")} disabled={not @menu_editor.can_move_down?}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="move-down" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="move-down" phx-target={@myself} title={dgettext("ui", "menuEditor.moveDown")} disabled={not @menu_editor.can_move_down?}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M7 3h2v6h3l-4 4-4-4h3V3z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M7 3h2v6h3l-4 4-4-4h3V3z" /></svg>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="indent" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="indent" phx-target={@myself} title={dgettext("ui", "menuEditor.indent")} disabled={not @menu_editor.can_indent?}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="indent" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="indent" phx-target={@myself} title={dgettext("ui", "menuEditor.indent")} disabled={not @menu_editor.can_indent?}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 4h8v2H2V4zm0 3h4v2H2V7zm0 3h8v2H2v-2zm6-1 3 2-3 2V9z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 4h8v2H2V4zm0 3h4v2H2V7zm0 3h8v2H2v-2zm6-1 3 2-3 2V9z" /></svg>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="unindent" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="unindent" phx-target={@myself} title={dgettext("ui", "menuEditor.unindent")} disabled={not @menu_editor.can_unindent?}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="unindent" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="unindent" phx-target={@myself} title={dgettext("ui", "menuEditor.unindent")} disabled={not @menu_editor.can_unindent?}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 4h8v2H2V4zm0 3h4v2H2V7zm0 3h8v2H2v-2zm3-1-3 2 3 2V9z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 4h8v2H2V4zm0 3h4v2H2V7zm0 3h8v2H2v-2zm3-1-3 2 3 2V9z" /></svg>
</button> </button>
<button class="menu-editor-tool" data-testid="menu-editor-toolbar-button" data-action="delete" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="delete" phx-target={@myself} title={dgettext("ui", "menuEditor.delete")} disabled={not @menu_editor.can_delete?}> <button class="menu-editor-tool inline-flex h-9 min-w-9 items-center justify-center" data-testid="menu-editor-toolbar-button" data-action="delete" type="button" phx-click="menu_editor_toolbar_action" phx-value-action="delete" phx-target={@myself} title={dgettext("ui", "menuEditor.delete")} disabled={not @menu_editor.can_delete?}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M6 2h4l1 1h3v2H2V3h3l1-1zm-1 4h2v6H5V6zm4 0h2v6H9V6z" /></svg> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M6 2h4l1 1h3v2H2V3h3l1-1zm-1 4h2v6H5V6zm4 0h2v6H9V6z" /></svg>
</button> </button>
</div> </div>
<%= if @menu_editor.items == [] do %> <%= if @menu_editor.items == [] do %>
<div class="menu-editor-empty"><%= dgettext("ui", "menuEditor.empty") %></div> <div class="menu-editor-empty flex min-h-0 flex-1 items-center justify-center"><%= dgettext("ui", "menuEditor.empty") %></div>
<% else %> <% else %>
<div id="menu-editor-tree-shell" class="menu-editor-tree-shell" phx-hook="MenuEditorTree"> <div id="menu-editor-tree-shell" class="menu-editor-tree-shell min-h-0 flex-1 overflow-auto" phx-hook="MenuEditorTree">
<ul class="menu-editor-tree-level"> <ul class="menu-editor-tree-level">
<.menu_tree_level items={@menu_editor.items} menu_editor={@menu_editor} depth={0} myself={@myself} /> <.menu_tree_level items={@menu_editor.items} menu_editor={@menu_editor} depth={0} myself={@myself} />
</ul> </ul>

View File

@@ -1,10 +1,10 @@
<div class={["misc-editor-shell", misc_class(@misc_editor.kind)]} data-testid="misc-editor"> <div class={["misc-editor-shell flex h-full min-h-0 flex-col overflow-hidden", misc_class(@misc_editor.kind)]} data-testid="misc-editor">
<div class="misc-editor-header"> <div class="misc-editor-header flex shrink-0 items-start justify-between gap-3">
<div> <div>
<h2><%= @misc_editor.title %></h2> <h2><%= @misc_editor.title %></h2>
<p><%= @misc_editor.subtitle %></p> <p><%= @misc_editor.subtitle %></p>
</div> </div>
<div class="misc-editor-actions"> <div class="misc-editor-actions flex flex-wrap items-center justify-end gap-2">
<%= if refreshable?(@misc_editor.kind) do %> <%= if refreshable?(@misc_editor.kind) do %>
<button class="secondary" type="button" phx-click="rerun_misc_editor" phx-target={@myself}><%= dgettext("ui", "Refresh") %></button> <button class="secondary" type="button" phx-click="rerun_misc_editor" phx-target={@myself}><%= dgettext("ui", "Refresh") %></button>
<% end %> <% end %>
@@ -17,13 +17,13 @@
</div> </div>
</div> </div>
<div class="misc-editor-summary"> <div class="misc-editor-summary flex flex-wrap gap-2">
<%= for {label, value} <- summary_items(@misc_editor) do %> <%= for {label, value} <- summary_items(@misc_editor) do %>
<div class="misc-summary-pill"><span><%= label %></span><strong><%= value %></strong></div> <div class="misc-summary-pill"><span><%= label %></span><strong><%= value %></strong></div>
<% end %> <% end %>
</div> </div>
<div class="misc-editor-content"> <div class="misc-editor-content min-h-0 flex-1 overflow-auto">
<%= case @misc_editor.kind do %> <%= case @misc_editor.kind do %>
<% :documentation -> %> <% :documentation -> %>
<div class="documentation-view"> <div class="documentation-view">

View File

@@ -30,10 +30,10 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
~H""" ~H"""
<%= if Enum.any?(@editor_toolbar_buttons) do %> <%= if Enum.any?(@editor_toolbar_buttons) do %>
<div class="editor-toolbar"> <div class="editor-toolbar flex items-center gap-2">
<%= for button <- @editor_toolbar_buttons do %> <%= for button <- @editor_toolbar_buttons do %>
<button <button
class={["editor-toolbar-button", if(button.destructive, do: "is-destructive")]} class={["editor-toolbar-button inline-flex items-center justify-center", if(button.destructive, do: "is-destructive")]}
data-testid="editor-toolbar-overlay-button" data-testid="editor-toolbar-overlay-button"
type="button" type="button"
phx-click="open_overlay" phx-click="open_overlay"
@@ -55,10 +55,10 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
<span><%= dgettext("ui", "No background tasks running") %></span> <span><%= dgettext("ui", "No background tasks running") %></span>
</div> </div>
<% else %> <% else %>
<div class="task-list"> <div class="task-list flex flex-col gap-2">
<%= for task <- Map.get(@task_status, :tasks, []) do %> <%= for task <- Map.get(@task_status, :tasks, []) do %>
<div class="panel-entry task-entry"> <div class="panel-entry task-entry flex flex-col gap-2">
<div class="task-entry-header"> <div class="task-entry-header flex items-center justify-between gap-2">
<strong><%= task.name %></strong> <strong><%= task.name %></strong>
<span class={"task-status task-status-#{task.status}"}><%= Map.get(task, :status_label, task.status |> to_string() |> String.capitalize()) %></span> <span class={"task-status task-status-#{task.status}"}><%= Map.get(task, :status_label, task.status |> to_string() |> String.capitalize()) %></span>
</div> </div>
@@ -84,7 +84,7 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
<span><%= dgettext("ui", "No shell output yet") %></span> <span><%= dgettext("ui", "No shell output yet") %></span>
</div> </div>
<% else %> <% else %>
<div class="output-list"> <div class="output-list flex flex-col gap-2">
<%= for entry <- @output_entries do %> <%= for entry <- @output_entries do %>
<div class={[ <div class={[
"panel-entry", "panel-entry",
@@ -118,7 +118,7 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
<span><%= dgettext("ui", "No post links yet") %></span> <span><%= dgettext("ui", "No post links yet") %></span>
</div> </div>
<% else %> <% else %>
<div class="git-log-list"> <div class="git-log-list flex flex-col gap-2">
<%= if Enum.any?(@backlinks) do %> <%= if Enum.any?(@backlinks) do %>
<div class="panel-entry"><strong><%= dgettext("ui", "Backlinks") %></strong></div> <div class="panel-entry"><strong><%= dgettext("ui", "Backlinks") %></strong></div>
<%= for entry <- @backlinks do %> <%= for entry <- @backlinks do %>
@@ -165,7 +165,7 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do
~H""" ~H"""
<%= if Enum.empty?(@git_entries) do %> <%= if Enum.empty?(@git_entries) do %>
<div class="git-log-list"> <div class="git-log-list flex flex-col gap-2">
<div class="panel-entry panel-empty-state"> <div class="panel-entry panel-empty-state">
<strong><%= dgettext("ui", "Git Log") %></strong> <strong><%= dgettext("ui", "Git Log") %></strong>
<span><%= dgettext("ui", "No git history yet") %></span> <span><%= dgettext("ui", "No git history yet") %></span>

View File

@@ -1,15 +1,15 @@
<div class="post-editor editor" data-testid="post-editor"> <div class="post-editor editor flex h-full min-h-0 flex-col" data-testid="post-editor">
<div class="editor-header"> <div class="editor-header flex shrink-0 items-start justify-between gap-3">
<div class="editor-tabs"> <div class="editor-tabs flex min-w-0 flex-1 overflow-hidden">
<div class={["editor-tab", "active", if(@post_editor.dirty?, do: "dirty")]}> <div class={["editor-tab active inline-flex max-w-full items-center gap-2 overflow-hidden px-3 py-2", if(@post_editor.dirty?, do: "dirty")]}>
<span class="editor-tab-title" data-testid="editor-title"><%= @post_editor.display_title %></span> <span class="editor-tab-title truncate" data-testid="editor-title"><%= @post_editor.display_title %></span>
<%= if @post_editor.dirty? do %> <%= if @post_editor.dirty? do %>
<span class="editor-tab-dirty" title={dgettext("ui", "Unsaved")}>●</span> <span class="editor-tab-dirty" title={dgettext("ui", "Unsaved")}>●</span>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="editor-actions"> <div class="editor-actions flex flex-wrap items-center justify-end gap-2">
<span class={["status-badge", "status-#{@post_editor.status}"]} data-testid="post-status-badge"> <span class={["status-badge", "status-#{@post_editor.status}"]} data-testid="post-status-badge">
<%= post_status_label(@post_editor.status) %> <%= post_status_label(@post_editor.status) %>
</span> </span>
@@ -17,9 +17,9 @@
<span class="auto-save-indicator"><%= post_editor_save_state_label(@post_editor.save_state) %></span> <span class="auto-save-indicator"><%= post_editor_save_state_label(@post_editor.save_state) %></span>
<% end %> <% end %>
<div class="quick-actions-wrapper"> <div class="quick-actions-wrapper relative">
<button <button
class="secondary quick-actions-btn" class="secondary quick-actions-btn inline-flex items-center gap-2"
type="button" type="button"
phx-click="toggle_post_editor_quick_actions" phx-click="toggle_post_editor_quick_actions"
phx-target={@myself} phx-target={@myself}
@@ -29,9 +29,9 @@
</button> </button>
<%= if @post_editor.quick_actions_open? do %> <%= if @post_editor.quick_actions_open? do %>
<div class="quick-actions-menu"> <div class="quick-actions-menu absolute right-0 top-full z-10 mt-2 flex min-w-72 flex-col">
<button <button
class="quick-action-item" class="quick-action-item flex items-start gap-3 text-left"
data-testid="editor-toolbar-overlay-button" data-testid="editor-toolbar-overlay-button"
type="button" type="button"
phx-click="open_overlay" phx-click="open_overlay"
@@ -39,7 +39,7 @@
disabled={not @post_editor.detect_language_enabled?} disabled={not @post_editor.detect_language_enabled?}
> >
<span class="quick-action-icon">🤖</span> <span class="quick-action-icon">🤖</span>
<span class="quick-action-text"> <span class="quick-action-text flex min-w-0 flex-1 flex-col">
<strong><%= dgettext("ui", "AI Suggestions") %></strong> <strong><%= dgettext("ui", "AI Suggestions") %></strong>
<small><%= dgettext("ui", "Review title, excerpt, and content suggestions") %></small> <small><%= dgettext("ui", "Review title, excerpt, and content suggestions") %></small>
</span> </span>
@@ -48,7 +48,7 @@
<div class="quick-actions-divider"></div> <div class="quick-actions-divider"></div>
<button <button
class="quick-action-item" class="quick-action-item flex items-start gap-3 text-left"
data-testid="editor-toolbar-overlay-button" data-testid="editor-toolbar-overlay-button"
type="button" type="button"
phx-click="open_overlay" phx-click="open_overlay"
@@ -56,7 +56,7 @@
disabled={not @post_editor.can_translate?} disabled={not @post_editor.can_translate?}
> >
<span class="quick-action-icon">🌍</span> <span class="quick-action-icon">🌍</span>
<span class="quick-action-text"> <span class="quick-action-text flex min-w-0 flex-1 flex-col">
<strong><%= dgettext("ui", "Translate") %></strong> <strong><%= dgettext("ui", "Translate") %></strong>
<small><%= dgettext("ui", "Select a target language for this post") %></small> <small><%= dgettext("ui", "Select a target language for this post") %></small>
</span> </span>
@@ -83,14 +83,14 @@
</div> </div>
</div> </div>
<form class="post-editor-form editor-content" data-testid="post-editor-form" phx-change="change_post_editor" phx-target={@myself}> <form class="post-editor-form editor-content flex min-h-0 flex-1 flex-col gap-4 overflow-auto" data-testid="post-editor-form" phx-change="change_post_editor" phx-target={@myself}>
<div class="metadata-toggle-header"> <div class="metadata-toggle-header flex items-center justify-between gap-3">
<button class={["metadata-toggle", if(@post_editor.metadata_expanded, do: "expanded")]} type="button" phx-click="toggle_post_metadata" phx-target={@myself}> <button class={["metadata-toggle", if(@post_editor.metadata_expanded, do: "expanded")]} type="button" phx-click="toggle_post_metadata" phx-target={@myself}>
<span class="metadata-toggle-chevron"><%= if @post_editor.metadata_expanded, do: "▼", else: "▶" %></span> <span class="metadata-toggle-chevron"><%= if @post_editor.metadata_expanded, do: "▼", else: "▶" %></span>
<span><%= dgettext("ui", "Metadata") %></span> <span><%= dgettext("ui", "Metadata") %></span>
</button> </button>
<div class="editor-translations-flags" aria-label={dgettext("ui", "Translations")}> <div class="editor-translations-flags flex flex-wrap items-center gap-2" aria-label={dgettext("ui", "Translations")}>
<%= for flag <- @post_editor.translation_flags do %> <%= for flag <- @post_editor.translation_flags do %>
<button <button
class={[ class={[
@@ -111,18 +111,18 @@
</div> </div>
</div> </div>
<div class={["editor-header-row", if(not @post_editor.metadata_expanded, do: "is-collapsed")]}> <div class={["editor-header-row grid gap-4 xl:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)]", if(not @post_editor.metadata_expanded, do: "is-collapsed")]}>
<div class="editor-meta"> <div class="editor-meta flex min-w-0 flex-col gap-4">
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Title") %></label> <label><%= dgettext("ui", "Title") %></label>
<input class="post-editor-input" type="text" name="post_editor[title]" value={@post_editor.form["title"]} /> <input class="post-editor-input" type="text" name="post_editor[title]" value={@post_editor.form["title"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Tags") %></label> <label><%= dgettext("ui", "Tags") %></label>
<div class="tag-input-container"> <div class="tag-input-container relative">
<input type="hidden" name="post_editor[tags]" value={@post_editor.form["tags"]} /> <input type="hidden" name="post_editor[tags]" value={@post_editor.form["tags"]} />
<div class="tag-input-wrapper"> <div class="tag-input-wrapper flex flex-wrap items-center gap-2">
<%= for tag <- @post_editor.tag_chips do %> <%= for tag <- @post_editor.tag_chips do %>
<span class={["tag-chip", if(tag.color, do: "has-color")]} style={tag_chip_style(tag.color)}> <span class={["tag-chip", if(tag.color, do: "has-color")]} style={tag_chip_style(tag.color)}>
<span><%= tag.name %></span> <span><%= tag.name %></span>
@@ -141,7 +141,7 @@
</div> </div>
<%= if String.trim(@post_editor.tag_query || "") != "" and (Enum.any?(@post_editor.tag_suggestions) or @post_editor.tag_query_addable?) do %> <%= if String.trim(@post_editor.tag_query || "") != "" and (Enum.any?(@post_editor.tag_suggestions) or @post_editor.tag_query_addable?) do %>
<div class="tag-suggestions"> <div class="tag-suggestions mt-2 flex flex-col">
<%= for tag <- @post_editor.tag_suggestions do %> <%= for tag <- @post_editor.tag_suggestions do %>
<button class="tag-suggestion" type="button" phx-click="add_post_editor_tag" phx-value-tag={tag.name} phx-target={@myself}> <button class="tag-suggestion" type="button" phx-click="add_post_editor_tag" phx-value-tag={tag.name} phx-target={@myself}>
<%= if tag.color do %> <%= if tag.color do %>
@@ -162,14 +162,14 @@
</div> </div>
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Author") %></label> <label><%= dgettext("ui", "Author") %></label>
<input class="post-editor-input" type="text" name="post_editor[author]" value={@post_editor.form["author"]} /> <input class="post-editor-input" type="text" name="post_editor[author]" value={@post_editor.form["author"]} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Language") %></label> <label><%= dgettext("ui", "Language") %></label>
<div class="editor-language-row"> <div class="editor-language-row flex items-center gap-2">
<select class="post-editor-input" name="post_editor[language]"> <select class="post-editor-input" name="post_editor[language]">
<%= for language <- @post_editor.languages do %> <%= for language <- @post_editor.languages do %>
<option value={language} selected={language == @post_editor.form["language"]}><%= String.upcase(language) %></option> <option value={language} selected={language == @post_editor.form["language"]}><%= String.upcase(language) %></option>
@@ -189,7 +189,7 @@
</div> </div>
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label class="editor-checkbox-label"> <label class="editor-checkbox-label">
<input type="hidden" name="post_editor[do_not_translate]" value="false" /> <input type="hidden" name="post_editor[do_not_translate]" value="false" />
<input type="checkbox" name="post_editor[do_not_translate]" value="true" checked={@post_editor.form["do_not_translate"]} /> <input type="checkbox" name="post_editor[do_not_translate]" value="true" checked={@post_editor.form["do_not_translate"]} />
@@ -197,17 +197,17 @@
</label> </label>
</div> </div>
<div class="editor-field-row"> <div class="editor-field-row grid gap-4 md:grid-cols-2">
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Slug") %></label> <label><%= dgettext("ui", "Slug") %></label>
<input class="post-editor-input is-readonly" type="text" readonly value={@post_editor.slug} /> <input class="post-editor-input is-readonly" type="text" readonly value={@post_editor.slug} />
</div> </div>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Categories") %></label> <label><%= dgettext("ui", "Categories") %></label>
<div class="tag-input-container"> <div class="tag-input-container relative">
<input type="hidden" name="post_editor[categories]" value={@post_editor.form["categories"]} /> <input type="hidden" name="post_editor[categories]" value={@post_editor.form["categories"]} />
<div class="tag-input-wrapper"> <div class="tag-input-wrapper flex flex-wrap items-center gap-2">
<%= for category <- @post_editor.category_values do %> <%= for category <- @post_editor.category_values do %>
<span class="tag-chip"> <span class="tag-chip">
<span><%= category %></span> <span><%= category %></span>
@@ -226,7 +226,7 @@
</div> </div>
<%= if String.trim(@post_editor.category_query || "") != "" and (Enum.any?(@post_editor.category_suggestions) or @post_editor.category_query_addable?) do %> <%= if String.trim(@post_editor.category_query || "") != "" and (Enum.any?(@post_editor.category_suggestions) or @post_editor.category_query_addable?) do %>
<div class="tag-suggestions"> <div class="tag-suggestions mt-2 flex flex-col">
<%= for category <- @post_editor.category_suggestions do %> <%= for category <- @post_editor.category_suggestions do %>
<button class="tag-suggestion" type="button" phx-click="add_post_editor_category" phx-value-category={category} phx-target={@myself}> <button class="tag-suggestion" type="button" phx-click="add_post_editor_category" phx-value-category={category} phx-target={@myself}>
<span class="tag-suggestion-name"><%= category %></span> <span class="tag-suggestion-name"><%= category %></span>
@@ -246,7 +246,7 @@
</div> </div>
<%= if @post_editor.show_template_selector? do %> <%= if @post_editor.show_template_selector? do %>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Template") %></label> <label><%= dgettext("ui", "Template") %></label>
<select class="post-editor-input" name="post_editor[template_slug]"> <select class="post-editor-input" name="post_editor[template_slug]">
<option value=""><%= dgettext("ui", "Default") %></option> <option value=""><%= dgettext("ui", "Default") %></option>
@@ -257,9 +257,9 @@
</div> </div>
<% end %> <% end %>
<div class="post-editor-links-panel"> <div class="post-editor-links-panel flex flex-col gap-3">
<strong><%= dgettext("ui", "Post Links") %></strong> <strong><%= dgettext("ui", "Post Links") %></strong>
<div class="post-editor-links-columns"> <div class="post-editor-links-columns grid gap-4 md:grid-cols-2">
<div> <div>
<span class="post-editor-links-label"><%= dgettext("ui", "Backlinks") %></span> <span class="post-editor-links-label"><%= dgettext("ui", "Backlinks") %></span>
<%= if Enum.any?(@post_editor.post_links.backlinks) do %> <%= if Enum.any?(@post_editor.post_links.backlinks) do %>
@@ -288,15 +288,15 @@
</div> </div>
</div> </div>
<aside class="editor-media-panel post-editor-side-panel"> <aside class="editor-media-panel post-editor-side-panel flex flex-col gap-3">
<div class="post-editor-side-panel-header"> <div class="post-editor-side-panel-header">
<strong><%= dgettext("ui", "Linked Media") %></strong> <strong><%= dgettext("ui", "Linked Media") %></strong>
</div> </div>
<%= if Enum.any?(@post_editor.linked_media) do %> <%= if Enum.any?(@post_editor.linked_media) do %>
<ul class="post-editor-media-list"> <ul class="post-editor-media-list flex flex-col gap-2">
<%= for item <- @post_editor.linked_media do %> <%= for item <- @post_editor.linked_media do %>
<li class="post-editor-media-item"> <li class="post-editor-media-item flex flex-col gap-1">
<span class="post-editor-media-title"><%= item.name %></span> <span class="post-editor-media-title"><%= item.name %></span>
<span class="post-editor-media-meta"><%= dgettext("ui", "Order") %>: <%= item.sort_order %></span> <span class="post-editor-media-meta"><%= dgettext("ui", "Order") %>: <%= item.sort_order %></span>
</li> </li>
@@ -314,19 +314,19 @@
</button> </button>
<div class={["editor-excerpt-panel", if(not @post_editor.excerpt_expanded, do: "is-collapsed")]}> <div class={["editor-excerpt-panel", if(not @post_editor.excerpt_expanded, do: "is-collapsed")]}>
<div class="editor-field"> <div class="editor-field flex flex-col gap-1.5">
<label><%= dgettext("ui", "Excerpt") %></label> <label><%= dgettext("ui", "Excerpt") %></label>
<textarea class="post-editor-textarea post-editor-excerpt" name="post_editor[excerpt]" rows="4"><%= @post_editor.form["excerpt"] %></textarea> <textarea class="post-editor-textarea post-editor-excerpt" name="post_editor[excerpt]" rows="4"><%= @post_editor.form["excerpt"] %></textarea>
</div> </div>
</div> </div>
<div class="editor-body"> <div class="editor-body flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="editor-toolbar"> <div class="editor-toolbar flex items-center gap-3">
<div class="editor-toolbar-left"> <div class="editor-toolbar-left flex items-center gap-2">
<label><%= dgettext("ui", "Content") %></label> <label><%= dgettext("ui", "Content") %></label>
</div> </div>
<div class="editor-toolbar-center"> <div class="editor-toolbar-center flex flex-1 justify-center">
<div class="editor-mode-toggle"> <div class="editor-mode-toggle">
<%= for mode <- [:markdown, :preview] do %> <%= for mode <- [:markdown, :preview] do %>
<button <button
@@ -342,7 +342,7 @@
</div> </div>
</div> </div>
<div class="editor-toolbar-right"> <div class="editor-toolbar-right flex items-center gap-2">
<%= if @post_editor.mode == :markdown do %> <%= if @post_editor.mode == :markdown do %>
<button <button
class="insert-post-link-button" class="insert-post-link-button"
@@ -379,7 +379,7 @@
</div> </div>
<%= if @post_editor.mode == :preview do %> <%= if @post_editor.mode == :preview do %>
<div class="editor-preview post-editor-preview" data-testid="post-editor-preview"> <div class="editor-preview post-editor-preview flex min-h-0 flex-1" data-testid="post-editor-preview">
<%= if @post_editor.preview_url do %> <%= if @post_editor.preview_url do %>
<iframe class="editor-preview-frame" src={@post_editor.preview_url}></iframe> <iframe class="editor-preview-frame" src={@post_editor.preview_url}></iframe>
<% else %> <% else %>
@@ -398,14 +398,14 @@
data-monaco-word-wrap="on" data-monaco-word-wrap="on"
data-monaco-insert-event="post-editor-insert-content" data-monaco-insert-event="post-editor-insert-content"
> >
<div id={"post-editor-monaco-#{@post_editor.id}"} class="monaco-editor-instance" phx-update="ignore"></div> <div id={"post-editor-monaco-#{@post_editor.id}"} class="monaco-editor-instance min-h-0 flex-1" phx-update="ignore"></div>
<textarea id={"post-editor-content-#{@post_editor.id}"} class="monaco-editor-input post-editor-content" data-testid="post-editor-content" data-post-editor-id={@post_editor.id} name="post_editor[content]" rows="18" spellcheck="false"><%= @post_editor.form["content"] %></textarea> <textarea id={"post-editor-content-#{@post_editor.id}"} class="monaco-editor-input post-editor-content" data-testid="post-editor-content" data-post-editor-id={@post_editor.id} name="post_editor[content]" rows="18" spellcheck="false"><%= @post_editor.form["content"] %></textarea>
</div> </div>
<% end %> <% end %>
</div> </div>
</form> </form>
<div class="editor-footer"> <div class="editor-footer flex shrink-0 flex-wrap gap-4">
<span><strong><%= dgettext("ui", "Created") %>:</strong> <%= @post_editor.footer.created_at %></span> <span><strong><%= dgettext("ui", "Created") %>:</strong> <%= @post_editor.footer.created_at %></span>
<span><strong><%= dgettext("ui", "Updated") %>:</strong> <%= @post_editor.footer.updated_at %></span> <span><strong><%= dgettext("ui", "Updated") %>:</strong> <%= @post_editor.footer.updated_at %></span>
<%= if @post_editor.footer.published_at do %> <%= if @post_editor.footer.published_at do %>

View File

@@ -1,7 +1,7 @@
<div class="scripts-view-shell editor" data-testid="script-editor"> <div class="scripts-view-shell editor flex h-full min-h-0 flex-col" data-testid="script-editor">
<div class="editor-header scripts-header"> <div class="editor-header scripts-header flex shrink-0 items-start justify-between gap-3">
<div class="editor-tabs"><div class="editor-tab active"><span class="editor-tab-title"><%= @script_editor.title %></span></div></div> <div class="editor-tabs flex min-w-0 flex-1 overflow-hidden"><div class="editor-tab active inline-flex max-w-full items-center overflow-hidden px-3 py-2"><span class="editor-tab-title truncate"><%= @script_editor.title %></span></div></div>
<div class="editor-actions"> <div class="editor-actions flex flex-wrap items-center justify-end gap-2">
<span class={[ <span class={[
"status-badge", "status-badge",
"status-#{@script_editor.status}" "status-#{@script_editor.status}"
@@ -15,35 +15,35 @@
<button class="secondary danger" type="button" phx-click="delete_script_editor" phx-target={@myself}><%= dgettext("ui", "Delete") %></button> <button class="secondary danger" type="button" phx-click="delete_script_editor" phx-target={@myself}><%= dgettext("ui", "Delete") %></button>
</div> </div>
</div> </div>
<form class="editor-content scripts-view" phx-change="change_script_editor" phx-target={@myself}> <form class="editor-content scripts-view flex min-h-0 flex-1 flex-col gap-4 overflow-hidden" phx-change="change_script_editor" phx-target={@myself}>
<div class="editor-header-row scripts-meta-row"> <div class="editor-header-row scripts-meta-row grid gap-4">
<div class="editor-meta"> <div class="editor-meta flex min-w-0 flex-col gap-4">
<div class="editor-field-row"> <div class="editor-field-row grid gap-4 md:grid-cols-2">
<div class="editor-field"><label><%= dgettext("ui", "Title") %></label><input type="text" name="script_editor[title]" value={@script_editor.title} /></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Title") %></label><input type="text" name="script_editor[title]" value={@script_editor.title} /></div>
<div class="editor-field"><label><%= dgettext("ui", "Slug") %></label><input type="text" name="script_editor[slug]" value={@script_editor.slug} /></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Slug") %></label><input type="text" name="script_editor[slug]" value={@script_editor.slug} /></div>
</div> </div>
<div class="editor-field-row"> <div class="editor-field-row grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
<div class="editor-field"><label><%= dgettext("ui", "Kind") %></label><select name="script_editor[kind]"><option value="utility" selected={@script_editor.kind == "utility"}>utility</option><option value="macro" selected={@script_editor.kind == "macro"}>macro</option><option value="transform" selected={@script_editor.kind == "transform"}>transform</option></select></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Kind") %></label><select name="script_editor[kind]"><option value="utility" selected={@script_editor.kind == "utility"}>utility</option><option value="macro" selected={@script_editor.kind == "macro"}>macro</option><option value="transform" selected={@script_editor.kind == "transform"}>transform</option></select></div>
<div class="editor-field"><label><%= dgettext("ui", "Entrypoint") %></label><select name="script_editor[entrypoint]"><%= for entrypoint <- @script_editor.entrypoints do %><option value={entrypoint} selected={entrypoint == @script_editor.entrypoint}><%= entrypoint %></option><% end %></select></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Entrypoint") %></label><select name="script_editor[entrypoint]"><%= for entrypoint <- @script_editor.entrypoints do %><option value={entrypoint} selected={entrypoint == @script_editor.entrypoint}><%= entrypoint %></option><% end %></select></div>
<div class="editor-field scripts-enabled-field"><label><input type="checkbox" name="script_editor[enabled]" checked={@script_editor.enabled} /> <%= dgettext("ui", "Enabled") %></label></div> <div class="editor-field scripts-enabled-field flex flex-col justify-end gap-1.5"><label><input type="checkbox" name="script_editor[enabled]" checked={@script_editor.enabled} /> <%= dgettext("ui", "Enabled") %></label></div>
</div> </div>
</div> </div>
</div> </div>
<div class="editor-body scripts-editor"> <div class="editor-body scripts-editor flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="editor-toolbar scripts-toolbar"><div class="editor-toolbar-left"><label><%= dgettext("ui", "Content") %></label></div></div> <div class="editor-toolbar scripts-toolbar flex items-center gap-3"><div class="editor-toolbar-left flex items-center gap-2"><label><%= dgettext("ui", "Content") %></label></div></div>
<div <div
id={"script-editor-monaco-shell-#{@script_editor.id}"} id={"script-editor-monaco-shell-#{@script_editor.id}"}
class="scripts-monaco monaco-editor-shell" class="scripts-monaco monaco-editor-shell min-h-0 flex-1 overflow-hidden"
phx-hook="MonacoEditor" phx-hook="MonacoEditor"
data-monaco-editor-id={@script_editor.id} data-monaco-editor-id={@script_editor.id}
data-monaco-input-id={"script-editor-content-#{@script_editor.id}"} data-monaco-input-id={"script-editor-content-#{@script_editor.id}"}
data-monaco-language="lua" data-monaco-language="lua"
data-monaco-word-wrap="on" data-monaco-word-wrap="on"
> >
<div id={"script-editor-monaco-#{@script_editor.id}"} class="monaco-editor-instance" phx-update="ignore"></div> <div id={"script-editor-monaco-#{@script_editor.id}"} class="monaco-editor-instance min-h-0 flex-1" phx-update="ignore"></div>
<textarea id={"script-editor-content-#{@script_editor.id}"} class="monaco-editor-input code-editor-textarea" name="script_editor[content]" spellcheck="false"><%= @script_editor.content %></textarea> <textarea id={"script-editor-content-#{@script_editor.id}"} class="monaco-editor-input code-editor-textarea" name="script_editor[content]" spellcheck="false"><%= @script_editor.content %></textarea>
</div> </div>
</div> </div>
<div class="editor-footer"><span class="text-muted text-small"><%= dgettext("ui", "Created") %>: <%= BDS.Persistence.timestamp_to_iso8601(@script_editor.created_at) %></span><span class="text-muted text-small"><%= dgettext("ui", "Updated") %>: <%= BDS.Persistence.timestamp_to_iso8601(@script_editor.updated_at) %></span></div> <div class="editor-footer flex shrink-0 flex-wrap gap-4"><span class="text-muted text-small"><%= dgettext("ui", "Created") %>: <%= BDS.Persistence.timestamp_to_iso8601(@script_editor.created_at) %></span><span class="text-muted text-small"><%= dgettext("ui", "Updated") %>: <%= BDS.Persistence.timestamp_to_iso8601(@script_editor.updated_at) %></span></div>
</form> </form>
</div> </div>

View File

@@ -1,22 +1,22 @@
<div <div
id="settings-editor-shell" id="settings-editor-shell"
class="settings-view-shell" class="settings-view-shell flex h-full min-h-0 flex-col overflow-hidden"
data-testid="settings-editor" data-testid="settings-editor"
phx-hook="SettingsSectionScroll" phx-hook="SettingsSectionScroll"
data-selected-settings-section={@settings_editor.selected_section} data-selected-settings-section={@settings_editor.selected_section}
data-settings-scroll-target={"settings-section-#{@settings_editor.selected_section}"} data-settings-scroll-target={"settings-section-#{@settings_editor.selected_section}"}
> >
<div class="settings-view"> <div class="settings-view flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="settings-header"> <div class="settings-header flex shrink-0 items-center justify-between gap-3">
<h2 data-testid="editor-title"><%= dgettext("ui", "Settings") %></h2> <h2 data-testid="editor-title"><%= dgettext("ui", "Settings") %></h2>
<form class="settings-search" phx-change="change_settings_search" phx-target={@myself}> <form class="settings-search w-full max-w-xs" phx-change="change_settings_search" phx-target={@myself}>
<input type="text" name="query" value={@settings_editor.search_query} placeholder={dgettext("ui", "Search settings")} /> <input type="text" name="query" value={@settings_editor.search_query} placeholder={dgettext("ui", "Search settings")} />
</form> </form>
</div> </div>
<div class="settings-content"> <div class="settings-content min-h-0 flex-1 overflow-auto">
<%= if Enum.empty?(@settings_editor.active_sections) do %> <%= if Enum.empty?(@settings_editor.active_sections) do %>
<div class="settings-no-results"> <div class="settings-no-results flex items-center justify-center py-6">
<p><%= dgettext("ui", "No settings match the current search") %></p> <p><%= dgettext("ui", "No settings match the current search") %></p>
</div> </div>
<% end %> <% end %>

View File

@@ -56,25 +56,25 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
if is_map(filters) and Map.get(filters, :enabled) do if is_map(filters) and Map.get(filters, :enabled) do
~H""" ~H"""
<form class="search-box" data-testid="sidebar-search-form" phx-change="update_sidebar_search" phx-submit="update_sidebar_search"> <form class="search-box flex items-center gap-2" data-testid="sidebar-search-form" phx-change="update_sidebar_search" phx-submit="update_sidebar_search">
<input <input
type="text" type="text"
name="sidebar_filters[search]" name="sidebar_filters[search]"
value={Map.get(@selected_filters, :search) || ""} value={Map.get(@selected_filters, :search) || ""}
placeholder={@sidebar_filters_config.search_placeholder} placeholder={@sidebar_filters_config.search_placeholder}
/> />
<button type="submit" title={dgettext("ui", "Search")}> <button class="inline-flex h-8 w-8 items-center justify-center" type="submit" title={dgettext("ui", "Search")}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"> <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M15.7 14.3l-4.2-4.2c-.2-.2-.5-.3-.8-.3.9-1.1 1.5-2.5 1.5-4C12.2 2.6 9.6 0 6.4 0S.6 2.6.6 5.8s2.6 5.8 5.8 5.8c1.5 0 2.9-.5 4-1.4 0 .3.1.6.3.8l4.2 4.2c.2.2.5.3.7.3s.5-.1.7-.3c.4-.4.4-1 0-1.4zm-9.3-4c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5z"/> <path d="M15.7 14.3l-4.2-4.2c-.2-.2-.5-.3-.8-.3.9-1.1 1.5-2.5 1.5-4C12.2 2.6 9.6 0 6.4 0S.6 2.6.6 5.8s2.6 5.8 5.8 5.8c1.5 0 2.9-.5 4-1.4 0 .3.1.6.3.8l4.2 4.2c.2.2.5.3.7.3s.5-.1.7-.3c.4-.4.4-1 0-1.4zm-9.3-4c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5z"/>
</svg> </svg>
</button> </button>
<%= if Map.get(@selected_filters, :search) do %> <%= if Map.get(@selected_filters, :search) do %>
<button class="clear-search" data-testid="sidebar-clear-search" type="button" phx-click="clear_sidebar_search">×</button> <button class="clear-search inline-flex h-8 w-8 items-center justify-center" data-testid="sidebar-clear-search" type="button" phx-click="clear_sidebar_search">×</button>
<% end %> <% end %>
</form> </form>
<%= if Map.get(@sidebar_filters_config, :has_active_filters) do %> <%= if Map.get(@sidebar_filters_config, :has_active_filters) do %>
<div class="filter-status"> <div class="filter-status flex items-center justify-between gap-2">
<span> <span>
<%= @sidebar_filters_config.results_label %>: <%= @sidebar_filters_config.loaded_count %>/<%= @sidebar_filters_config.total_count %> <%= @sidebar_filters_config.results_label %>: <%= @sidebar_filters_config.loaded_count %>/<%= @sidebar_filters_config.total_count %>
</span> </span>
@@ -239,7 +239,7 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
if Map.get(filters, :has_more) do if Map.get(filters, :has_more) do
~H""" ~H"""
<div class="sidebar-load-more"> <div class="sidebar-load-more flex justify-center pt-2">
<button class="load-more-button" data-testid="sidebar-load-more" type="button" phx-click="load_more_sidebar"> <button class="load-more-button" data-testid="sidebar-load-more" type="button" phx-click="load_more_sidebar">
<%= dgettext("ui", "Load more") %> <%= dgettext("ui", "Load more") %>
</button> </button>
@@ -270,9 +270,9 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
<span data-testid="sidebar-section-title"><%= section.title %></span> <span data-testid="sidebar-section-title"><%= section.title %></span>
<span class="sidebar-section-count"><%= Map.get(section, :count, length(Map.get(section, :items, []))) %></span> <span class="sidebar-section-count"><%= Map.get(section, :count, length(Map.get(section, :items, []))) %></span>
</div> </div>
<div class="sidebar-list"> <div class="sidebar-list flex flex-col">
<%= for item <- Map.get(section, :items, []) do %> <%= for item <- Map.get(section, :items, []) do %>
<div class="sidebar-item-row" data-item-id={item.id}> <div class="sidebar-item-row flex items-center gap-2" data-item-id={item.id}>
<button <button
class={["sidebar-item", "sidebar-post-item", "post-type-post", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]} class={["sidebar-item", "sidebar-post-item", "post-type-post", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]}
data-testid="sidebar-open-item" data-testid="sidebar-open-item"

View File

@@ -1,27 +1,27 @@
<div <div
id="tags-editor-shell" id="tags-editor-shell"
class="tags-view-shell" class="tags-view-shell flex h-full min-h-0 flex-col overflow-hidden"
data-testid="tags-editor" data-testid="tags-editor"
phx-hook="TagsSectionScroll" phx-hook="TagsSectionScroll"
data-selected-tags-section={@tags_editor.selected_section} data-selected-tags-section={@tags_editor.selected_section}
data-tags-scroll-target={"tags-section-#{@tags_editor.selected_section}"} data-tags-scroll-target={"tags-section-#{@tags_editor.selected_section}"}
> >
<div class="tags-view"> <div class="tags-view flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="tags-view-header"> <div class="tags-view-header flex shrink-0 items-center justify-between gap-3">
<h2><%= dgettext("ui", "Tags") %></h2> <h2><%= dgettext("ui", "Tags") %></h2>
</div> </div>
<div class="tags-view-content"> <div class="tags-view-content flex min-h-0 flex-1 flex-col gap-4 overflow-auto">
<div class="tags-section" id="tags-section-cloud"> <div class="tags-section" id="tags-section-cloud">
<div class="tags-section-header"><h3><%= dgettext("ui", "Tag Cloud") %></h3></div> <div class="tags-section-header"><h3><%= dgettext("ui", "Tag Cloud") %></h3></div>
<div class="tags-section-content"> <div class="tags-section-content">
<%= if Enum.empty?(@tags_editor.tags) do %> <%= if Enum.empty?(@tags_editor.tags) do %>
<div class="tags-empty-state"> <div class="tags-empty-state flex flex-col gap-3">
<p><%= dgettext("ui", "No tags found") %></p> <p><%= dgettext("ui", "No tags found") %></p>
<button class="secondary" type="button" phx-click="sync_tags_editor" phx-target={@myself}><%= dgettext("ui", "Discover") %></button> <button class="secondary" type="button" phx-click="sync_tags_editor" phx-target={@myself}><%= dgettext("ui", "Discover") %></button>
</div> </div>
<% else %> <% else %>
<div class="tag-cloud"> <div class="tag-cloud flex flex-wrap gap-2">
<%= for tag <- @tags_editor.tags do %> <%= for tag <- @tags_editor.tags do %>
<button class={["tag-cloud-item", if(tag.name in @tags_editor.selected, do: "selected"), if(tag.color, do: "has-color")]} style={tag_style(tag, @tags_editor.tags)} type="button" phx-click="toggle_tag_selection" phx-value-name={tag.name} phx-target={@myself}> <button class={["tag-cloud-item", if(tag.name in @tags_editor.selected, do: "selected"), if(tag.color, do: "has-color")]} style={tag_style(tag, @tags_editor.tags)} type="button" phx-click="toggle_tag_selection" phx-value-name={tag.name} phx-target={@myself}>
<%= tag.name %><span class="tag-count"><%= tag.count %></span> <%= tag.name %><span class="tag-count"><%= tag.count %></span>
@@ -36,7 +36,7 @@
<div class="tags-section-header"><h3><%= dgettext("ui", "Create / Edit") %></h3></div> <div class="tags-section-header"><h3><%= dgettext("ui", "Create / Edit") %></h3></div>
<div class="tags-section-content"> <div class="tags-section-content">
<form class="tag-create-form" phx-change="change_new_tag_editor" phx-target={@myself}> <form class="tag-create-form" phx-change="change_new_tag_editor" phx-target={@myself}>
<div class="tag-form-row"> <div class="tag-form-row flex flex-wrap items-center gap-3">
<input type="text" name="new_tag[name]" value={@tags_editor.new_tag["name"]} placeholder={dgettext("ui", "Tag name")} /> <input type="text" name="new_tag[name]" value={@tags_editor.new_tag["name"]} placeholder={dgettext("ui", "Tag name")} />
<input type="color" name="new_tag[color]" value={if(@tags_editor.new_tag["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.new_tag["color"])} /> <input type="color" name="new_tag[color]" value={if(@tags_editor.new_tag["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.new_tag["color"])} />
<button class="primary" type="button" phx-click="create_tag_editor" phx-target={@myself}><%= dgettext("ui", "Create") %></button> <button class="primary" type="button" phx-click="create_tag_editor" phx-target={@myself}><%= dgettext("ui", "Create") %></button>
@@ -45,7 +45,7 @@
<%= if @tags_editor.edit_draft != %{} do %> <%= if @tags_editor.edit_draft != %{} do %>
<form class="tag-edit-form" phx-change="change_edit_tag_editor" phx-target={@myself}> <form class="tag-edit-form" phx-change="change_edit_tag_editor" phx-target={@myself}>
<div class="tag-form-row"> <div class="tag-form-row flex flex-wrap items-center gap-3">
<input type="text" name="edit_tag[name]" value={@tags_editor.edit_draft["name"]} /> <input type="text" name="edit_tag[name]" value={@tags_editor.edit_draft["name"]} />
<input type="color" name="edit_tag[color]" value={if(@tags_editor.edit_draft["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.edit_draft["color"])} /> <input type="color" name="edit_tag[color]" value={if(@tags_editor.edit_draft["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.edit_draft["color"])} />
<select name="edit_tag[post_template_slug]"> <select name="edit_tag[post_template_slug]">
@@ -65,8 +65,8 @@
<div class="tags-section" id="tags-section-merge"> <div class="tags-section" id="tags-section-merge">
<div class="tags-section-header"><h3><%= dgettext("ui", "Merge Tags") %></h3></div> <div class="tags-section-header"><h3><%= dgettext("ui", "Merge Tags") %></h3></div>
<div class="tags-section-content"> <div class="tags-section-content">
<div class="merge-form"> <div class="merge-form flex flex-col gap-3">
<div class="tag-form-row"> <div class="tag-form-row flex flex-wrap items-center gap-3">
<select phx-change="change_merge_target" name="target" phx-target={@myself}> <select phx-change="change_merge_target" name="target" phx-target={@myself}>
<%= for tag_name <- @tags_editor.selected do %> <%= for tag_name <- @tags_editor.selected do %>
<option value={tag_name} selected={tag_name == @tags_editor.merge_target}><%= tag_name %></option> <option value={tag_name} selected={tag_name == @tags_editor.merge_target}><%= tag_name %></option>

View File

@@ -1,7 +1,7 @@
<div class="templates-view-shell editor" data-testid="template-editor"> <div class="templates-view-shell editor flex h-full min-h-0 flex-col" data-testid="template-editor">
<div class="editor-header templates-header"> <div class="editor-header templates-header flex shrink-0 items-start justify-between gap-3">
<div class="editor-tabs"><div class="editor-tab active"><span class="editor-tab-title"><%= @template_editor.title %></span></div></div> <div class="editor-tabs flex min-w-0 flex-1 overflow-hidden"><div class="editor-tab active inline-flex max-w-full items-center overflow-hidden px-3 py-2"><span class="editor-tab-title truncate"><%= @template_editor.title %></span></div></div>
<div class="editor-actions"> <div class="editor-actions flex flex-wrap items-center justify-end gap-2">
<span class={[ <span class={[
"status-badge", "status-badge",
"status-#{@template_editor.status}" "status-#{@template_editor.status}"
@@ -14,34 +14,34 @@
<button class="secondary danger" type="button" phx-click="delete_template_editor" phx-target={@myself}><%= dgettext("ui", "Delete") %></button> <button class="secondary danger" type="button" phx-click="delete_template_editor" phx-target={@myself}><%= dgettext("ui", "Delete") %></button>
</div> </div>
</div> </div>
<form class="editor-content templates-view" phx-change="change_template_editor" phx-target={@myself}> <form class="editor-content templates-view flex min-h-0 flex-1 flex-col gap-4 overflow-hidden" phx-change="change_template_editor" phx-target={@myself}>
<div class="editor-header-row templates-meta-row"> <div class="editor-header-row templates-meta-row grid gap-4">
<div class="editor-meta"> <div class="editor-meta flex min-w-0 flex-col gap-4">
<div class="editor-field-row"> <div class="editor-field-row grid gap-4 md:grid-cols-2">
<div class="editor-field"><label><%= dgettext("ui", "Title") %></label><input type="text" name="template_editor[title]" value={@template_editor.title} /></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Title") %></label><input type="text" name="template_editor[title]" value={@template_editor.title} /></div>
<div class="editor-field"><label><%= dgettext("ui", "Slug") %></label><input type="text" name="template_editor[slug]" value={@template_editor.slug} /></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Slug") %></label><input type="text" name="template_editor[slug]" value={@template_editor.slug} /></div>
</div> </div>
<div class="editor-field-row"> <div class="editor-field-row grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
<div class="editor-field"><label><%= dgettext("ui", "Kind") %></label><select name="template_editor[kind]"><option value="post" selected={@template_editor.kind == :post or @template_editor.kind == "post"}>post</option><option value="list" selected={@template_editor.kind == :list or @template_editor.kind == "list"}>list</option><option value="not-found" selected={@template_editor.kind == :"not-found" or @template_editor.kind == "not-found"}>not-found</option><option value="partial" selected={@template_editor.kind == :partial or @template_editor.kind == "partial"}>partial</option></select></div> <div class="editor-field flex flex-col gap-1.5"><label><%= dgettext("ui", "Kind") %></label><select name="template_editor[kind]"><option value="post" selected={@template_editor.kind == :post or @template_editor.kind == "post"}>post</option><option value="list" selected={@template_editor.kind == :list or @template_editor.kind == "list"}>list</option><option value="not-found" selected={@template_editor.kind == :"not-found" or @template_editor.kind == "not-found"}>not-found</option><option value="partial" selected={@template_editor.kind == :partial or @template_editor.kind == "partial"}>partial</option></select></div>
<div class="editor-field templates-enabled-field"><label><input type="checkbox" name="template_editor[enabled]" checked={@template_editor.enabled} /> <%= dgettext("ui", "Enabled") %></label></div> <div class="editor-field templates-enabled-field flex flex-col justify-end gap-1.5"><label><input type="checkbox" name="template_editor[enabled]" checked={@template_editor.enabled} /> <%= dgettext("ui", "Enabled") %></label></div>
</div> </div>
</div> </div>
</div> </div>
<div class="editor-body templates-editor"> <div class="editor-body templates-editor flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="editor-toolbar templates-toolbar"><div class="editor-toolbar-left"><label><%= dgettext("ui", "Content") %></label></div></div> <div class="editor-toolbar templates-toolbar flex items-center gap-3"><div class="editor-toolbar-left flex items-center gap-2"><label><%= dgettext("ui", "Content") %></label></div></div>
<div <div
id={"template-editor-monaco-shell-#{@template_editor.id}"} id={"template-editor-monaco-shell-#{@template_editor.id}"}
class="templates-monaco monaco-editor-shell" class="templates-monaco monaco-editor-shell min-h-0 flex-1 overflow-hidden"
phx-hook="MonacoEditor" phx-hook="MonacoEditor"
data-monaco-editor-id={@template_editor.id} data-monaco-editor-id={@template_editor.id}
data-monaco-input-id={"template-editor-content-#{@template_editor.id}"} data-monaco-input-id={"template-editor-content-#{@template_editor.id}"}
data-monaco-language="liquid" data-monaco-language="liquid"
data-monaco-word-wrap="on" data-monaco-word-wrap="on"
> >
<div id={"template-editor-monaco-#{@template_editor.id}"} class="monaco-editor-instance" phx-update="ignore"></div> <div id={"template-editor-monaco-#{@template_editor.id}"} class="monaco-editor-instance min-h-0 flex-1" phx-update="ignore"></div>
<textarea id={"template-editor-content-#{@template_editor.id}"} class="monaco-editor-input code-editor-textarea" name="template_editor[content]" spellcheck="false"><%= @template_editor.content %></textarea> <textarea id={"template-editor-content-#{@template_editor.id}"} class="monaco-editor-input code-editor-textarea" name="template_editor[content]" spellcheck="false"><%= @template_editor.content %></textarea>
</div> </div>
</div> </div>
<div class="editor-footer"><span class="text-muted text-small"><%= dgettext("ui", "Created") %>: <%= BDS.Persistence.timestamp_to_iso8601(@template_editor.created_at) %></span><span class="text-muted text-small"><%= dgettext("ui", "Updated") %>: <%= BDS.Persistence.timestamp_to_iso8601(@template_editor.updated_at) %></span></div> <div class="editor-footer flex shrink-0 flex-wrap gap-4"><span class="text-muted text-small"><%= dgettext("ui", "Created") %>: <%= BDS.Persistence.timestamp_to_iso8601(@template_editor.created_at) %></span><span class="text-muted text-small"><%= dgettext("ui", "Updated") %>: <%= BDS.Persistence.timestamp_to_iso8601(@template_editor.updated_at) %></span></div>
</form> </form>
</div> </div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLiveTest do
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
@shell_live_source_root Path.expand("../../../lib/bds/desktop/shell_live", __DIR__) @shell_live_source_root Path.expand("../../../lib/bds/desktop/shell_live", __DIR__)
@endpoint BDS.Desktop.Endpoint
@css_source_files [ @css_source_files [
"tokens.css", "tokens.css",
"shell.css", "shell.css",
@@ -28,6 +29,210 @@ defmodule BDS.Desktop.ShellLiveTest do
|> Enum.join("\n") |> Enum.join("\n")
end end
defp phase3_post_editor_assigns do
%{
myself: nil,
post_editor: %{
id: 42,
dirty?: true,
display_title: "Phase 3 Post",
status: :draft,
save_state: :saving,
quick_actions_open?: false,
can_publish?: true,
discard_title: "Discard draft",
discard_label: "Discard",
can_delete?: true,
metadata_expanded: true,
translation_flags: [
%{language: "en", status: :draft, active: true, label: "English", flag: "EN"}
],
form: %{
"title" => "Phase 3 Post",
"author" => "Author",
"language" => "en",
"do_not_translate" => false,
"template_slug" => "",
"excerpt" => "Excerpt",
"content" => "# Hello",
"tags" => "elixir",
"categories" => "news"
},
tag_chips: [%{name: "elixir", color: "#3b82f6"}],
tag_query: "",
tag_suggestions: [],
tag_query_addable?: false,
languages: ["en", "de"],
detect_language_enabled?: true,
slug: "phase-3-post",
category_values: ["news"],
category_query: "",
category_suggestions: [],
category_query_addable?: false,
show_template_selector?: true,
template_options: [%{slug: "default", title: "Default"}],
post_links: %{backlinks: [], outlinks: []},
linked_media: [],
excerpt_expanded: true,
mode: :markdown,
gallery_count: 1,
preview_url: nil,
footer: %{created_at: "2026-05-04", updated_at: "2026-05-04", published_at: nil},
can_translate?: true
}
}
end
defp phase3_media_editor_assigns do
%{
myself: nil,
media_editor: %{
dirty?: true,
display_title: "Hero Image",
save_state: :saved,
quick_actions_open?: false,
is_image: true,
can_detect_language?: true,
can_translate?: true,
preview_url: "/media/hero.jpg",
form: %{
"title" => "Hero Image",
"alt" => "Hero alt",
"caption" => "Caption",
"tags" => "cover",
"author" => "Author",
"language" => "en"
},
original_name: "hero.jpg",
mime_type: "image/jpeg",
file_size: "42 KB",
dimensions: "1200x800",
languages: ["en", "de"],
translations: [],
post_picker_open?: false,
post_picker_query: "",
post_picker_results: [],
post_picker_overflow_count: 0,
linked_posts: [],
editing_translation: nil
}
}
end
defp phase3_script_editor_assigns do
%{
myself: nil,
script_editor: %{
id: 7,
title: "Build Feed",
slug: "build-feed",
kind: "utility",
entrypoint: "run",
enabled: true,
content: "print('ok')",
entrypoints: ["run"],
status: :draft,
can_publish?: true,
created_at: 1_714_816_000,
updated_at: 1_714_816_000
}
}
end
defp phase3_template_editor_assigns do
%{
myself: nil,
template_editor: %{
id: 9,
title: "Post Template",
slug: "post-template",
kind: :post,
enabled: true,
content: "{{ content }}",
status: :draft,
can_publish?: true,
created_at: 1_714_816_000,
updated_at: 1_714_816_000
}
}
end
defp phase3_chat_editor_assigns do
%{
myself: nil,
chat_editor: %{
id: 5,
needs_api_key?: false,
title: "AI Assistant",
effective_model: "gpt-4.1",
model_selector_open?: false,
available_models: [],
available_model_groups: [],
messages: [],
is_streaming: false,
pending_user_message: nil,
streaming_content: "",
streaming_tool_markers: [],
streaming_inline_surfaces: [],
input: "",
send_disabled?: true,
action_error: nil
}
}
end
defp phase3_menu_editor_assigns do
%{
myself: nil,
menu_editor: %{
draft: nil,
title: "Navigation",
description: "Manage site navigation",
can_move_up?: false,
can_move_down?: false,
can_indent?: false,
can_unindent?: false,
can_delete?: false,
items: []
}
}
end
defp phase3_settings_editor_assigns do
%{
myself: nil,
current_tab: %{type: :settings, id: "settings"},
settings_editor: %{
selected_section: "project",
search_query: "",
active_sections: [],
project_visible?: false,
editor_visible?: false,
content_visible?: false,
ai_visible?: false,
publishing_visible?: false,
data_visible?: false,
technology_visible?: false,
mcp_visible?: false
}
}
end
defp phase3_tags_editor_assigns do
%{
myself: nil,
tags_editor: %{
selected_section: "cloud",
tags: [],
new_tag: %{"name" => "", "color" => "#3b82f6"},
edit_draft: %{},
selected: [],
merge_target: nil,
templates: []
}
}
end
test "shell live modules use contexts instead of direct Repo.get calls" do test "shell live modules use contexts instead of direct Repo.get calls" do
source_files = source_files =
[ [
@@ -51,6 +256,56 @@ defmodule BDS.Desktop.ShellLiveTest do
assert offenders == [] assert offenders == []
end end
@tag :phase3
test "phase 3 shell chrome renders utility-owned layout classes" do
conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint)
{:ok, _view, html} = live_isolated(conn, BDS.Desktop.ShellLive)
assert html =~ "activity-bar flex h-full shrink-0 flex-col items-center"
assert html =~ "sidebar-shell flex min-w-0 shrink-0 overflow-hidden"
assert html =~ "tab-bar-empty flex h-full items-center px-3 text-sm"
assert html =~ "assistant-sidebar-shell flex min-w-0 shrink-0 overflow-hidden"
assert html =~ "status-bar flex h-[22px] shrink-0 items-center justify-between"
end
@tag :phase3
test "phase 3 editors and shared surfaces render utility-owned layouts" do
post_html = render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
script_html = render_component(&BDS.Desktop.ShellLive.ScriptEditor.render/1, phase3_script_editor_assigns())
template_html = render_component(&BDS.Desktop.ShellLive.TemplateEditor.render/1, phase3_template_editor_assigns())
chat_html = render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html = render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
settings_html = render_component(&BDS.Desktop.ShellLive.SettingsEditor.render/1, phase3_settings_editor_assigns())
tags_html = render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
assert post_html =~ "post-editor editor flex h-full min-h-0 flex-col"
assert post_html =~ "editor-header flex shrink-0 items-start justify-between gap-3"
assert post_html =~ "editor-field flex flex-col gap-1.5"
assert post_html =~ "editor-toolbar flex items-center gap-3"
assert media_html =~ "media-editor editor flex h-full min-h-0 flex-col"
assert media_html =~ "editor-content media-editor grid min-h-0 flex-1 gap-4"
assert script_html =~ "scripts-view-shell editor flex h-full min-h-0 flex-col"
assert script_html =~ "editor-content scripts-view flex min-h-0 flex-1 flex-col gap-4 overflow-hidden"
assert template_html =~ "templates-view-shell editor flex h-full min-h-0 flex-col"
assert template_html =~ "editor-content templates-view flex min-h-0 flex-1 flex-col gap-4 overflow-hidden"
assert chat_html =~ "chat-panel flex h-full min-h-0 flex-col"
assert chat_html =~ "chat-panel-header flex shrink-0 items-center justify-between gap-3"
assert menu_html =~ "menu-editor-view flex h-full min-h-0 flex-col"
assert menu_html =~ "menu-editor-toolbar flex flex-wrap items-center gap-2"
assert settings_html =~ "settings-view-shell flex h-full min-h-0 flex-col overflow-hidden"
assert settings_html =~ "settings-header flex shrink-0 items-center justify-between gap-3"
assert tags_html =~ "tags-view-shell flex h-full min-h-0 flex-col overflow-hidden"
assert tags_html =~ "tag-form-row flex flex-wrap items-center gap-3"
end
alias BDS.Persistence alias BDS.Persistence
alias BDS.AI alias BDS.AI
alias BDS.CliSync.Watcher alias BDS.CliSync.Watcher
@@ -191,8 +446,6 @@ defmodule BDS.Desktop.ShellLiveTest do
end end
end end
@endpoint BDS.Desktop.Endpoint
setup do setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
@@ -694,7 +947,7 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="sidebar-shell") assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(data-testid="status-bar") assert html =~ ~s(data-testid="status-bar")
assert html =~ ~s(data-testid="status-task-button") assert html =~ ~s(data-testid="status-task-button")
assert html =~ ~s(class="panel-shell is-hidden") assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ ~s(data-testid="activity-button") assert html =~ ~s(data-testid="activity-button")
assert html =~ ~s(data-view="posts") assert html =~ ~s(data-view="posts")
assert html =~ ~s(data-view="media") assert html =~ ~s(data-view="media")
@@ -711,14 +964,14 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='toggle-sidebar']") |> element("[data-testid='toggle-sidebar']")
|> render_click() |> render_click()
assert html =~ ~s(class="sidebar-shell is-hidden") assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
html = html =
view view
|> element("[data-testid='toggle-sidebar']") |> element("[data-testid='toggle-sidebar']")
|> render_click() |> render_click()
refute html =~ ~s(class="sidebar-shell is-hidden") refute html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
html = html =
view view
@@ -726,7 +979,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click() |> render_click()
assert html =~ ~s(data-region="panel") assert html =~ ~s(data-region="panel")
refute html =~ ~s(class="panel-shell is-hidden") refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ ~s(data-testid="panel-close") assert html =~ ~s(data-testid="panel-close")
html = html =
@@ -734,7 +987,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='panel-close']") |> element("[data-testid='panel-close']")
|> render_click() |> render_click()
assert html =~ ~s(class="panel-shell is-hidden") assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = html =
view view
@@ -765,7 +1018,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click() |> render_click()
refute html =~ ~s(data-tab-type="settings") refute html =~ ~s(data-tab-type="settings")
assert html =~ ~s(class="tab-bar-empty") assert html =~ ~s(class="tab-bar-empty flex h-full items-center px-3 text-sm")
end end
test "macos hides the custom titlebar and moves shell toggles into the status bar" do test "macos hides the custom titlebar and moves shell toggles into the status bar" do
@@ -785,7 +1038,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='toggle-sidebar']") |> element("[data-testid='toggle-sidebar']")
|> render_click() |> render_click()
assert html =~ ~s(class="sidebar-shell is-hidden") assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
html = html =
render_hook(view, "native_menu_action", %{"action" => "edit_preferences"}) render_hook(view, "native_menu_action", %{"action" => "edit_preferences"})
@@ -1053,7 +1306,7 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-tab-id="post-1") assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(data-tab-type="media") assert html =~ ~s(data-tab-type="media")
assert html =~ ~s(data-tab-id="media-1") assert html =~ ~s(data-tab-id="media-1")
assert html =~ ~s(class="tab active transient") assert Regex.match?(~r/class="tab [^"]*active[^"]*transient/, html)
end end
test "workbench session restore renders documentation tab content" do test "workbench session restore renders documentation tab content" do
@@ -1469,7 +1722,7 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-tab-type="post") assert html =~ ~s(data-tab-type="post")
assert html =~ ~s(data-tab-id="post-1") assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(class="tab active transient") assert Regex.match?(~r/class="tab [^"]*active[^"]*transient/, html)
html = html =
render_click(view, "pin_sidebar_item", %{ render_click(view, "pin_sidebar_item", %{
@@ -1480,7 +1733,7 @@ defmodule BDS.Desktop.ShellLiveTest do
}) })
assert html =~ ~s(data-tab-id="post-1") assert html =~ ~s(data-tab-id="post-1")
refute html =~ ~s(class="tab active transient") refute Regex.match?(~r/class="tab [^"]*active[^"]*transient/, html)
html = html =
render_click(view, "open_sidebar_item", %{ render_click(view, "open_sidebar_item", %{
@@ -1521,13 +1774,13 @@ defmodule BDS.Desktop.ShellLiveTest do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell") assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(class="panel-shell is-hidden") assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "b", meta: true}) html = render_keydown(view, "shortcut", %{key: "b", meta: true})
assert html =~ ~s(class="sidebar-shell is-hidden") assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "j", meta: true}) html = render_keydown(view, "shortcut", %{key: "j", meta: true})
refute html =~ ~s(class="panel-shell is-hidden") refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "2", meta: true}) html = render_keydown(view, "shortcut", %{key: "2", meta: true})
assert html =~ ~s(data-view="media") assert html =~ ~s(data-view="media")
@@ -1557,7 +1810,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='toggle-sidebar']") |> element("[data-testid='toggle-sidebar']")
|> render_click() |> render_click()
assert html =~ ~s(class="sidebar-shell is-hidden") assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
assert html =~ ~s(style="width: 0px;") assert html =~ ~s(style="width: 0px;")
end end
@@ -1596,8 +1849,8 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="sidebar-search-form") assert html =~ ~s(data-testid="sidebar-search-form")
assert html =~ ~s(data-testid="sidebar-filter-toggle") assert html =~ ~s(data-testid="sidebar-filter-toggle")
assert html =~ ~s(class="sidebar-section-header") assert html =~ ~s(class="sidebar-section-header flex items-center justify-between gap-2")
assert html =~ ~s(class="sidebar-actions") assert html =~ ~s(class="sidebar-actions flex items-center gap-1")
assert html =~ ~s(data-testid="sidebar-load-more") assert html =~ ~s(data-testid="sidebar-load-more")
assert html_position(html, ~s(data-testid="sidebar-load-more")) > assert html_position(html, ~s(data-testid="sidebar-load-more")) >
@@ -1762,19 +2015,22 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Add published posts" assert html =~ "Add published posts"
html = render_click(view, "select_panel_tab", %{"tab" => "output"}) html = render_click(view, "select_panel_tab", %{"tab" => "output"})
refute html =~ ~s(class="panel-shell is-hidden") refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = html =
view view
|> element("[data-testid='status-task-button']") |> element("[data-testid='status-task-button']")
|> render_click() |> render_click()
refute html =~ ~s(class="panel-shell is-hidden") refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ assert Regex.match?(
~s(<button class="panel-tab active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">) ~r/<button class="panel-tab [^"]*active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">/,
html
)
assert html =~ ~s(class="task-list") or html =~ "No background tasks running" assert html =~ ~s(class="task-list flex flex-col gap-2") or
html =~ ~s(class="panel-entry panel-empty-state")
end end
test "metadata diff tasks localize task text, show progress, and open the diff result in the UI" do test "metadata diff tasks localize task text, show progress, and open the diff result in the UI" do
@@ -2273,7 +2529,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_change() |> render_change()
html = render(view) html = render(view)
assert html =~ ~s(class="tab active dirty") assert Regex.match?(~r/class="tab [^"]*active[^"]*dirty/, html)
assert html =~ "Updated Shell Post" assert html =~ "Updated Shell Post"
_html = render_hook(view, "native_menu_action", %{"action" => "save"}) _html = render_hook(view, "native_menu_action", %{"action" => "save"})
@@ -2868,7 +3124,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "published" "subtitle" => "published"
}) })
assert published_script_html =~ ~s(class="scripts-view-shell editor") assert published_script_html =~ ~s(class="scripts-view-shell editor flex h-full min-h-0 flex-col")
assert published_script_html =~ ~s(data-testid="script-editor") assert published_script_html =~ ~s(data-testid="script-editor")
assert published_script_html =~ ~s(data-testid="script-status-badge") assert published_script_html =~ ~s(data-testid="script-status-badge")
assert published_script_html =~ ~s(class="status-badge status-published") assert published_script_html =~ ~s(class="status-badge status-published")
@@ -2889,7 +3145,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "published" "subtitle" => "published"
}) })
assert published_template_html =~ ~s(class="templates-view-shell editor") assert published_template_html =~ ~s(class="templates-view-shell editor flex h-full min-h-0 flex-col")
assert published_template_html =~ ~s(data-testid="template-editor") assert published_template_html =~ ~s(data-testid="template-editor")
assert published_template_html =~ ~s(data-testid="template-status-badge") assert published_template_html =~ ~s(data-testid="template-status-badge")
assert published_template_html =~ ~s(class="status-badge status-published") assert published_template_html =~ ~s(class="status-badge status-published")
@@ -3082,8 +3338,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => media.original_name "subtitle" => media.original_name
}) })
assert html =~ ~s(class="editor-content media-editor") assert html =~
assert html =~ ~s(class="quick-actions-wrapper") "class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
assert html =~ ~s(class="quick-actions-wrapper relative")
refute html =~ ~s(class="media-editor-form") refute html =~ ~s(class="media-editor-form")
assert has_element?( assert has_element?(
@@ -3107,7 +3364,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click() |> render_click()
assert html =~ ~s(class="translation-modal-backdrop") assert html =~ ~s(class="translation-modal-backdrop")
assert html =~ ~s(class="translation-modal") assert html =~ ~s(class="translation-modal flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden")
assert html =~ ~s(name="media_translation[title]") assert html =~ ~s(name="media_translation[title]")
assert html =~ ~s(name="media_translation[alt]") assert html =~ ~s(name="media_translation[alt]")
assert html =~ ~s(name="media_translation[caption]") assert html =~ ~s(name="media_translation[caption]")
@@ -3195,7 +3452,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "Project settings" "subtitle" => "Project settings"
}) })
assert settings_html =~ ~s(class="settings-view-shell") assert settings_html =~ ~s(class="settings-view-shell flex h-full min-h-0 flex-col overflow-hidden")
assert settings_html =~ ~s(class="setting-section") assert settings_html =~ ~s(class="setting-section")
refute settings_html =~ "Desktop workbench content routed through the Elixir shell." refute settings_html =~ "Desktop workbench content routed through the Elixir shell."
@@ -3207,7 +3464,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "Manage tags" "subtitle" => "Manage tags"
}) })
assert tags_html =~ ~s(class="tags-view-shell") assert tags_html =~ ~s(class="tags-view-shell flex h-full min-h-0 flex-col overflow-hidden")
assert tags_html =~ ~s(class="tags-section") assert tags_html =~ ~s(class="tags-section")
refute tags_html =~ "Desktop workbench content routed through the Elixir shell." refute tags_html =~ "Desktop workbench content routed through the Elixir shell."
@@ -3231,7 +3488,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => script.slug "subtitle" => script.slug
}) })
assert script_html =~ ~s(class="scripts-view-shell editor") assert script_html =~ ~s(class="scripts-view-shell editor flex h-full min-h-0 flex-col")
assert script_html =~ "scripts-monaco" assert script_html =~ "scripts-monaco"
assert script_html =~ ~s(data-monaco-language="lua") assert script_html =~ ~s(data-monaco-language="lua")
assert script_html =~ ~s(data-monaco-word-wrap="on") assert script_html =~ ~s(data-monaco-word-wrap="on")
@@ -3246,7 +3503,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => template.slug "subtitle" => template.slug
}) })
assert template_html =~ ~s(class="templates-view-shell editor") assert template_html =~ ~s(class="templates-view-shell editor flex h-full min-h-0 flex-col")
assert template_html =~ "templates-monaco" assert template_html =~ "templates-monaco"
assert template_html =~ ~s(data-monaco-language="liquid") assert template_html =~ ~s(data-monaco-language="liquid")
assert template_html =~ ~s(data-monaco-word-wrap="on") assert template_html =~ ~s(data-monaco-word-wrap="on")
@@ -3261,8 +3518,8 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => conversation.model || "chat" "subtitle" => conversation.model || "chat"
}) })
assert chat_html =~ ~s(class="chat-panel") assert chat_html =~ ~s(class="chat-panel flex h-full min-h-0 flex-col")
assert chat_html =~ ~s(class="chat-input-container") assert chat_html =~ ~s(class="chat-input-container flex shrink-0 flex-col gap-3")
refute chat_html =~ "Desktop workbench content routed through the Elixir shell." refute chat_html =~ "Desktop workbench content routed through the Elixir shell."
end end
@@ -3281,8 +3538,8 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="chat-model-selector-button") assert html =~ ~s(data-testid="chat-model-selector-button")
assert html =~ ~s(class="chat-panel-title-main") assert html =~ ~s(class="chat-panel-title-main")
assert html =~ ~s(class="chat-model-selector-wrap") assert html =~ ~s(class="chat-model-selector-wrap relative shrink-0")
assert html =~ ~s(class="chat-model-selector-button chat-model-selector-inline") assert html =~ ~s(class="chat-model-selector-button chat-model-selector-inline inline-flex items-center gap-2")
refute html =~ ~s(class="chat-panel-header-actions") refute html =~ ~s(class="chat-panel-header-actions")
css = desktop_css_source() css = desktop_css_source()
@@ -3330,7 +3587,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='chat-model-selector-button']") |> element("[data-testid='chat-model-selector-button']")
|> render_click() |> render_click()
assert selector_html =~ ~s(class="chat-model-selector-menu") assert selector_html =~ ~s(class="chat-model-selector-menu absolute right-0 top-full z-10 mt-2 flex min-w-56 flex-col")
assert selector_html =~ ~s(data-testid="chat-model-selector-option") assert selector_html =~ ~s(data-testid="chat-model-selector-option")
assert selector_html =~ "llama-current" assert selector_html =~ "llama-current"
@@ -3380,7 +3637,7 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => conversation.model || "chat" "subtitle" => conversation.model || "chat"
}) })
assert html =~ ~s(<span class="tab-title">New Chat</span>) assert html =~ ~s(<span class="tab-title truncate">New Chat</span>)
_html = _html =
view view
@@ -3408,10 +3665,10 @@ defmodule BDS.Desktop.ShellLiveTest do
end) end)
assert AI.get_chat_conversation(conversation.id).title == "Posts 2026" assert AI.get_chat_conversation(conversation.id).title == "Posts 2026"
assert html =~ ~s(<span class="tab-title">Posts 2026</span>) assert html =~ ~s(<span class="tab-title truncate">Posts 2026</span>)
assert html =~ ~r/<span class="chat-panel-title-main">\s*Posts 2026\s*<\/span>/ assert html =~ ~r/<span class="chat-panel-title-main">\s*Posts 2026\s*<\/span>/
assert html =~ ~s(<span class="chat-item-title">Posts 2026</span>) assert html =~ ~s(<span class="chat-item-title">Posts 2026</span>)
refute html =~ ~s(<span class="tab-title">New Chat</span>) refute html =~ ~s(<span class="tab-title truncate">New Chat</span>)
refute html =~ ~s(<span class="chat-item-title">New Chat</span>) refute html =~ ~s(<span class="chat-item-title">New Chat</span>)
end end

View File

@@ -261,16 +261,16 @@ defmodule BDS.DesktopTest do
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([])) conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
assert conn.status == 200 assert conn.status == 200
assert conn.resp_body =~ ~s(class="app") assert conn.resp_body =~ ~s(class="app flex h-full w-full flex-col")
refute conn.resp_body =~ ~s(data-testid="window-titlebar") refute conn.resp_body =~ ~s(data-testid="window-titlebar")
refute conn.resp_body =~ ~s(data-testid="window-titlebar-menu-bar") refute conn.resp_body =~ ~s(data-testid="window-titlebar-menu-bar")
assert conn.resp_body =~ ~s(data-testid="status-shell-controls") assert conn.resp_body =~ ~s(data-testid="status-shell-controls")
assert conn.resp_body =~ ~s(data-testid="toggle-sidebar") assert conn.resp_body =~ ~s(data-testid="toggle-sidebar")
assert conn.resp_body =~ ~s(data-testid="toggle-panel") assert conn.resp_body =~ ~s(data-testid="toggle-panel")
assert conn.resp_body =~ ~s(data-testid="toggle-assistant") assert conn.resp_body =~ ~s(data-testid="toggle-assistant")
assert conn.resp_body =~ ~s(class="activity-bar") assert conn.resp_body =~ ~s(class="activity-bar flex h-full shrink-0 flex-col items-center justify-between")
assert conn.resp_body =~ ~s(class="sidebar") assert conn.resp_body =~ ~s(class="sidebar flex min-w-0 flex-1 overflow-hidden")
assert conn.resp_body =~ ~s(class="status-bar") assert conn.resp_body =~ ~s(class="status-bar flex h-[22px] shrink-0 items-center justify-between gap-2")
assert conn.resp_body =~ ~s(data-phx-main) assert conn.resp_body =~ ~s(data-phx-main)
assert conn.resp_body =~ ~s(href="/assets/app.css") assert conn.resp_body =~ ~s(href="/assets/app.css")
assert conn.resp_body =~ ~s(src="/assets/app.js") assert conn.resp_body =~ ~s(src="/assets/app.js")

View File

@@ -140,6 +140,44 @@ defmodule BDS.UI.ShellTest do
assert template =~ "data-workbench-session={encoded_workbench_session(@workbench)}" assert template =~ "data-workbench-session={encoded_workbench_session(@workbench)}"
end end
test "tailwind source keeps theme tokens and shared component primitives" do
css = css_source()
app_css = File.read!("/Users/gb/Projects/bDS2/assets/css/app.css")
assert app_css =~ ~s|@import "tailwindcss" source(none);|
assert css =~ "@theme"
assert css =~ "--color-shell-bg:"
assert css =~ "--font-sans:"
assert css =~ "@layer components"
assert css =~ ".btn-base"
assert css =~ ".btn-theme-primary"
assert css =~ ".btn-theme-danger"
assert css =~ ".panel-entry"
assert css =~ ".monaco-host"
end
test "live javascript is split into focused Phoenix asset modules" do
app_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js")
assert app_js =~ ~s(import { createHooks } from "./hooks/index.js";)
assert app_js =~ ~s(import { syncTitlebarOverlayInsets } from "./bridges/titlebar_overlay.js";)
assert app_js =~ ~s(import { createMenuRuntimeCommandRunner } from "./bridges/menu_runtime.js";)
assert app_js =~ ~s(import { createMonacoServices } from "./monaco/services.js";)
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/index.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/menu_runtime.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/monaco/services.js")
end
test "top level shell render uses utility classes for common layout" do
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
assert template =~ ~s(class="app flex h-full w-full flex-col")
assert template =~ ~s(class="app-main flex min-h-0 flex-1 overflow-hidden")
assert template =~ ~s(class="app-content flex min-w-0 flex-1 flex-col overflow-hidden")
assert template =~ ~s(class="tab-bar flex h-[35px] shrink-0 items-center overflow-hidden")
end
test "desktop shell css keeps editor and help docs on the VS Code dark surface" do test "desktop shell css keeps editor and help docs on the VS Code dark surface" do
css = css_source() css = css_source()
@@ -261,6 +299,7 @@ defmodule BDS.UI.ShellTest do
test "desktop shell assets keep old activity, tab, focus, and titlebar overlay parity rules" do test "desktop shell assets keep old activity, tab, focus, and titlebar overlay parity rules" do
css = css_source() css = css_source()
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js")
titlebar_js = File.read!("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
assert css =~ "color: var(--vscode-activityBar-foreground)" assert css =~ "color: var(--vscode-activityBar-foreground)"
@@ -276,9 +315,9 @@ defmodule BDS.UI.ShellTest do
assert css =~ "justify-content: space-between" assert css =~ "justify-content: space-between"
assert css =~ "align-items: center" assert css =~ "align-items: center"
assert css =~ "padding-right: calc(10px + var(--bds-titlebar-overlay-right, 0px));" assert css =~ "padding-right: calc(10px + var(--bds-titlebar-overlay-right, 0px));"
assert live_js =~ "windowControlsOverlay" assert titlebar_js =~ "windowControlsOverlay"
assert live_js =~ "geometrychange" assert titlebar_js =~ "geometrychange"
assert live_js =~ "--bds-titlebar-overlay-left" assert titlebar_js =~ "--bds-titlebar-overlay-left"
assert live_js =~ "dataset.shortcuts" assert live_js =~ "dataset.shortcuts"
assert live_js =~ "addEventListener(\"keydown\", this.handleShortcutKeyDown, true)" assert live_js =~ "addEventListener(\"keydown\", this.handleShortcutKeyDown, true)"
assert live_js =~ "event.preventDefault()" assert live_js =~ "event.preventDefault()"
@@ -360,15 +399,15 @@ defmodule BDS.UI.ShellTest do
assert css =~ "opacity: 1;" assert css =~ "opacity: 1;"
assert Regex.match?( assert Regex.match?(
~r/class="secondary quick-actions-btn".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= dgettext\("ui", "Quick Actions"\) %><\/span>/s, ~r/class="secondary quick-actions-btn inline-flex items-center gap-2".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= dgettext\("ui", "Quick Actions"\) %><\/span>/s,
template template
) )
assert template =~ ~s(class="quick-action-text") assert template =~ ~s(class="quick-action-text flex min-w-0 flex-1 flex-col")
assert template =~ ~s(class="quick-action-icon">🤖</span>) assert template =~ ~s(class="quick-action-icon">🤖</span>)
assert Regex.match?( assert Regex.match?(
~r/class="quick-action-text">\s*<strong><%= dgettext\("ui", "AI Suggestions"\) %><\/strong>.*?<\/span>\s*<span class="quick-action-icon">🤖<\/span>/s, ~r/class="quick-action-text[^"]*">\s*<strong><%= dgettext\("ui", "AI Suggestions"\) %><\/strong>.*?<\/span>\s*<span class="quick-action-icon">🤖<\/span>/s,
template template
) )
@@ -480,7 +519,7 @@ defmodule BDS.UI.ShellTest do
assert post_editor_ex =~ "defp build_data(socket)" assert post_editor_ex =~ "defp build_data(socket)"
assert Regex.match?( assert Regex.match?(
~r/class="secondary quick-actions-btn".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= dgettext\("ui", "Quick Actions"\) %><\/span>/s, ~r/class="secondary quick-actions-btn inline-flex items-center gap-2".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= dgettext\("ui", "Quick Actions"\) %><\/span>/s,
post_template post_template
) )