From 35017f979384d001404f46c70f7f64fdd1603fcc Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Mon, 4 May 2026 11:12:17 +0200 Subject: [PATCH] feat: p hase 3 of tailwind migration --- assets/css/tokens.css | 18 + assets/css/utilities.css | 47 + assets/js/app.js | 127 +- assets/js/bridges/menu_runtime.js | 57 + assets/js/bridges/titlebar_overlay.js | 39 + assets/js/hooks/index.js | 1 + assets/js/monaco/services.js | 1 + .../chat_editor_html/chat_editor.html.heex | 32 +- lib/bds/desktop/shell_live/index.html.heex | 110 +- .../media_editor_html/media_editor.html.heex | 93 +- .../menu_editor_html/menu_editor.html.heex | 30 +- .../misc_editor_html/misc_editor.html.heex | 10 +- lib/bds/desktop/shell_live/panel_renderer.ex | 16 +- .../post_editor_html/post_editor.html.heex | 96 +- .../script_editor.html.heex | 38 +- .../settings_editor.html.heex | 12 +- .../desktop/shell_live/sidebar_components.ex | 14 +- .../tags_editor_html/tags_editor.html.heex | 20 +- .../template_editor.html.heex | 36 +- priv/static/assets/app.css | 5325 ++++++++- priv/static/assets/app.js | 9655 ++++++++++++++++- test/bds/desktop/shell_live_test.exs | 339 +- test/bds/desktop_test.exs | 8 +- test/bds/ui/shell_test.exs | 53 +- 24 files changed, 15752 insertions(+), 425 deletions(-) create mode 100644 assets/js/bridges/menu_runtime.js create mode 100644 assets/js/bridges/titlebar_overlay.js create mode 100644 assets/js/hooks/index.js create mode 100644 assets/js/monaco/services.js diff --git a/assets/css/tokens.css b/assets/css/tokens.css index 98748ab..7c6846d 100644 --- a/assets/css/tokens.css +++ b/assets/css/tokens.css @@ -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 { --accent-color: #007acc; --accent-color-transparent: rgba(0, 122, 204, 0.25); diff --git a/assets/css/utilities.css b/assets/css/utilities.css index e69de29..6ebb609 100644 --- a/assets/css/utilities.css +++ b/assets/css/utilities.css @@ -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; + } +} diff --git a/assets/js/app.js b/assets/js/app.js index 6931b5a..201e82e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,6 +1,10 @@ import { Socket } from "phoenix"; import { LiveSocket } from "phoenix_live_view"; 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", () => { const csrfToken = document @@ -119,46 +123,6 @@ document.addEventListener("DOMContentLoaded", () => { 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 liquidLanguageRegistered = false; let markdownWithMacrosRegistered = false; @@ -213,61 +177,12 @@ document.addEventListener("DOMContentLoaded", () => { document.documentElement.style.zoom = String(zoom); }; - const runMenuRuntimeCommand = (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; - } - }; + const menuRuntimeCommandRunner = createMenuRuntimeCommandRunner({ + activeMonacoEditor, + runMonacoEditorAction, + runDocumentCommand, + applyAppZoom + }); const cssVar = (name, fallback) => { const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim(); @@ -588,6 +503,8 @@ document.addEventListener("DOMContentLoaded", () => { return monacoLoaderPromise; }; + const monacoServices = createMonacoServices({ loadMonaco, ensureMonacoTheme }); + const Hooks = { AppShell: { mounted() { @@ -738,7 +655,7 @@ document.addEventListener("DOMContentLoaded", () => { this.handleEvent("menu-runtime-command", ({ action }) => { if (action) { - runMenuRuntimeCommand(String(action)); + menuRuntimeCommandRunner(String(action)); } }); @@ -1295,7 +1212,7 @@ document.addEventListener("DOMContentLoaded", () => { this.editor.focus(); }; - loadMonaco() + monacoServices.loadMonaco() .then(async (monaco) => { if (!this.host || !this.textarea) { return; @@ -1303,7 +1220,7 @@ document.addEventListener("DOMContentLoaded", () => { await this.waitForMonacoVisibleSize(); - ensureMonacoTheme(monaco); + monacoServices.ensureMonacoTheme(monaco); this.editor = monaco.editor.create(this.host, { value: this.textarea.value || "", @@ -1360,8 +1277,8 @@ document.addEventListener("DOMContentLoaded", () => { return; } - loadMonaco().then((monaco) => { - ensureMonacoTheme(monaco); + monacoServices.loadMonaco().then((monaco) => { + monacoServices.ensureMonacoTheme(monaco); monaco.editor.setTheme("bds-theme"); if (this.editor.getModel()?.getLanguageId() !== this.language) { @@ -1430,13 +1347,13 @@ document.addEventListener("DOMContentLoaded", () => { this.lastFilePath = this.filePath; }; - loadMonaco() + monacoServices.loadMonaco() .then((monaco) => { if (!this.host) { return; } - ensureMonacoTheme(monaco); + monacoServices.ensureMonacoTheme(monaco); this.editor = monaco.editor.createDiffEditor(this.host, { theme: "bds-theme", @@ -1470,8 +1387,8 @@ document.addEventListener("DOMContentLoaded", () => { return; } - loadMonaco().then((monaco) => { - ensureMonacoTheme(monaco); + monacoServices.loadMonaco().then((monaco) => { + monacoServices.ensureMonacoTheme(monaco); monaco.editor.setTheme("bds-theme"); this.editor.updateOptions({ @@ -1515,7 +1432,7 @@ document.addEventListener("DOMContentLoaded", () => { const liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }, - hooks: Hooks, + hooks: createHooks(Hooks), metadata: { keydown: (event) => ({ key: event.key, diff --git a/assets/js/bridges/menu_runtime.js b/assets/js/bridges/menu_runtime.js new file mode 100644 index 0000000..35083d6 --- /dev/null +++ b/assets/js/bridges/menu_runtime.js @@ -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; + } + }; +}; diff --git a/assets/js/bridges/titlebar_overlay.js b/assets/js/bridges/titlebar_overlay.js new file mode 100644 index 0000000..db31ab5 --- /dev/null +++ b/assets/js/bridges/titlebar_overlay.js @@ -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); + }; +}; diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js new file mode 100644 index 0000000..2567c71 --- /dev/null +++ b/assets/js/hooks/index.js @@ -0,0 +1 @@ +export const createHooks = (hooks) => hooks; diff --git a/assets/js/monaco/services.js b/assets/js/monaco/services.js new file mode 100644 index 0000000..e7bdbfa --- /dev/null +++ b/assets/js/monaco/services.js @@ -0,0 +1 @@ +export const createMonacoServices = (services) => services; diff --git a/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex b/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex index 98e6a06..49efdbb 100644 --- a/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex +++ b/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex @@ -1,6 +1,6 @@ -
-
-
+
+
+
<%= if @chat_editor.needs_api_key? do %> <%= dgettext("ui", "AI Chat Setup") %> @@ -10,9 +10,9 @@ <%= unless @chat_editor.needs_api_key? do %> - +
-
+
<%= if @chat_editor.needs_api_key? do %> -
+
🔑

<%= dgettext("ui", "API Key Required") %>

<%= dgettext("ui", "Configure an API key in Settings to enable AI chat.") %>

@@ -67,7 +67,7 @@
<% else %> <%= if Enum.empty?(@chat_editor.messages) and not @chat_editor.is_streaming do %> -
+
🤖

<%= dgettext("ui", "Welcome to the AI Assistant") %>

<%= dgettext("ui", "I can help you manage your blog with rich visualizations. Try asking me to:") %>

@@ -81,7 +81,7 @@
<% else %> <%= if @chat_editor.pending_user_message do %> -
+
👤
@@ -93,7 +93,7 @@ <% end %> <%= for message <- @chat_editor.messages do %> -
+
<%= if message.role == :user, do: "👤", else: "🤖" %>
<%= message_role_label(message.role) %>
@@ -113,7 +113,7 @@ <% end %> <%= if @chat_editor.is_streaming and (@chat_editor.streaming_content != "" or @chat_editor.streaming_tool_markers != []) do %> -
+
🤖
@@ -133,7 +133,7 @@ <% end %> <%= if @chat_editor.is_streaming and @chat_editor.streaming_content == "" and @chat_editor.streaming_tool_markers == [] do %> -
+
🤖
@@ -147,12 +147,12 @@
<%= unless @chat_editor.needs_api_key? do %> -
+
<%= if @chat_editor.is_streaming do %> <% end %> -
+
diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex index 211e374..e8e85d3 100644 --- a/lib/bds/desktop/shell_live/index.html.heex +++ b/lib/bds/desktop/shell_live/index.html.heex @@ -1,5 +1,5 @@
<% end %> -
-