feat: p hase 3 of tailwind migration
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
127
assets/js/app.js
127
assets/js/app.js
@@ -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,
|
||||||
|
|||||||
57
assets/js/bridges/menu_runtime.js
Normal file
57
assets/js/bridges/menu_runtime.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
39
assets/js/bridges/titlebar_overlay.js
Normal file
39
assets/js/bridges/titlebar_overlay.js
Normal 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
1
assets/js/hooks/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const createHooks = (hooks) => hooks;
|
||||||
1
assets/js/monaco/services.js
Normal file
1
assets/js/monaco/services.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const createMonacoServices = (services) => services;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user