diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index a07fa7dc2d..a66cb0aeb4 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -4800,7 +4800,7 @@ onUnmounted(() => {
@@ -7898,6 +7898,10 @@ onUnmounted(() => {
row-gap: 1rem;
}
+.settings-appearance-section {
+ container-type: inline-size;
+}
+
.settings-appearance-field > * + * {
margin-top: 0.5rem;
}
@@ -7920,10 +7924,22 @@ onUnmounted(() => {
.settings-appearance-theme-grid {
display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
+@container (max-width: 34rem) {
+ .settings-appearance-theme-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@container (max-width: 36rem) {
+ .settings-icon-theme-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
.settings-option-stack > * + * {
margin-top: 0.625rem;
}
diff --git a/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.choiceCardLayout.spec.ts b/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.choiceCardLayout.spec.ts
index a216e38e7d..9caa66b5af 100644
--- a/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.choiceCardLayout.spec.ts
+++ b/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.choiceCardLayout.spec.ts
@@ -118,12 +118,13 @@ describe("EditorSettingsDialog choice card containment", () => {
}
});
- it("keeps icon theme choices compact beside the corner style controls", () => {
- expect(dialogSource).toContain("grid-template-columns: repeat(3, minmax(0, 1fr));");
+ it("restores large icon theme choices below the debug log controls", () => {
+ expect(sourceIndexForKey("iconTheme")).toBeGreaterThan(sourceIndexForKey("debugLoggingEnabled"));
+ expect(templateSource).toContain("data-icon-theme-settings");
for (const key of ["iconThemeDefault", "iconThemeBlack"] as const) {
const block = buttonBlockForKey(key);
- expectClassTokens(classNameFromTag(openingTag(block, "Button")), ["settings-choice-button", "h-8"]);
- expect(block).toContain('class="h-7 w-7 shrink-0"');
+ expectClassTokens(classNameFromTag(openingTag(block, "Button")), ["settings-choice-card", "h-auto", "min-w-0", "whitespace-normal", "overflow-hidden"]);
+ expect(block).toContain('class="h-12 w-12 shrink-0"');
}
});
});
diff --git a/apps/desktop/src/components/layout/AppSidebar.vue b/apps/desktop/src/components/layout/AppSidebar.vue
index aff5733c83..c559beb738 100644
--- a/apps/desktop/src/components/layout/AppSidebar.vue
+++ b/apps/desktop/src/components/layout/AppSidebar.vue
@@ -268,26 +268,28 @@ defineExpose({ focusSearch, locateTabInSidebar });
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/layout/AppToolbar.vue b/apps/desktop/src/components/layout/AppToolbar.vue
index 95a57ad729..f4336db32f 100644
--- a/apps/desktop/src/components/layout/AppToolbar.vue
+++ b/apps/desktop/src/components/layout/AppToolbar.vue
@@ -2,12 +2,13 @@
import { computed, ref, onMounted, onBeforeUnmount, h, nextTick, watch } from "vue";
import { useI18n } from "vue-i18n";
import { invoke } from "@tauri-apps/api/core";
-import { DatabaseZap, FilePlus2, Loader2, Moon, Sun, SunMoon, History, Bot, ArrowLeftRight, FileCode, BookMarked, GitCompareArrows, TableProperties, Settings, CloudDownload, Package, FileDown, FolderTree } from "@lucide/vue";
+import { DatabaseZap, FilePlus2, Moon, Sun, SunMoon, History, Bot, ArrowLeftRight, FileCode, BookMarked, GitCompareArrows, TableProperties, Settings, CloudDownload, Package, FileDown, FolderTree } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import WindowControls from "@/components/layout/WindowControls.vue";
import ExportProgressPopover from "@/components/export/ExportProgressPopover.vue";
+import ToolbarUpdateIcon from "@/components/layout/ToolbarUpdateIcon.vue";
import { MAC_TRAFFIC_LIGHT_X, macTrafficLightInsetPaddingForScale, shouldReserveMacTrafficLightInset, useWindowControls } from "@/composables/useWindowControls";
import { useToast } from "@/composables/useToast";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -584,9 +585,8 @@ const toolbarStyle = computed(() => {
-
@@ -600,7 +600,15 @@ const toolbarStyle = computed(() => {
-
+
+
{{ t("sqlLibrary.title") }}
@@ -628,8 +637,9 @@ const toolbarStyle = computed(() => {
-
-
+
+
+
{{ t("sqlFileTree.title") }}
@@ -637,8 +647,9 @@ const toolbarStyle = computed(() => {
-
-
+
+
+
{{ t("history.title") }}
@@ -646,8 +657,8 @@ const toolbarStyle = computed(() => {
-
-
+
+
{
{{ activeAiRunCount > 9 ? "9+" : activeAiRunCount }}
+
AI
@@ -666,8 +678,8 @@ const toolbarStyle = computed(() => {
-
-
+
+
{{ t("toolbar.theme") }}
@@ -675,8 +687,8 @@ const toolbarStyle = computed(() => {
-
-
diff --git a/apps/desktop/src/components/layout/__tests__/ToolbarUpdateIcon.spec.ts b/apps/desktop/src/components/layout/__tests__/ToolbarUpdateIcon.spec.ts
new file mode 100644
index 0000000000..8c0330c697
--- /dev/null
+++ b/apps/desktop/src/components/layout/__tests__/ToolbarUpdateIcon.spec.ts
@@ -0,0 +1,37 @@
+// @vitest-environment happy-dom
+
+import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
+import { afterEach, describe, expect, it } from "vitest";
+import ToolbarUpdateIcon from "../ToolbarUpdateIcon.vue";
+
+const mountedApps: App[] = [];
+
+afterEach(() => {
+ for (const app of mountedApps.splice(0)) app.unmount();
+ document.body.innerHTML = "";
+});
+
+describe("ToolbarUpdateIcon", () => {
+ it("uses cloud download while idle and a cloud outline scan while checking", async () => {
+ const state = reactive({ loading: false });
+ const container = document.createElement("div");
+ document.body.append(container);
+ const app = createApp(
+ defineComponent({
+ setup: () => () => h(ToolbarUpdateIcon, { loading: state.loading }),
+ }),
+ );
+ mountedApps.push(app);
+ app.mount(container);
+ await nextTick();
+
+ expect(container.querySelector("[data-toolbar-update-idle]")).not.toBeNull();
+ expect(container.querySelector("[data-toolbar-update-cloud]")).toBeNull();
+
+ state.loading = true;
+ await nextTick();
+ expect(container.querySelector("[data-toolbar-update-idle]")).toBeNull();
+ expect(container.querySelector("[data-toolbar-update-cloud]")).not.toBeNull();
+ expect(container.querySelector("[data-toolbar-update-scan]")).not.toBeNull();
+ });
+});
diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue
index 1ebd5549b0..244f08c8d0 100644
--- a/apps/desktop/src/components/sidebar/ConnectionTree.vue
+++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue
@@ -1,7 +1,7 @@
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/sidebar/SidebarLocateButton.vue b/apps/desktop/src/components/sidebar/SidebarLocateButton.vue
new file mode 100644
index 0000000000..386b913bde
--- /dev/null
+++ b/apps/desktop/src/components/sidebar/SidebarLocateButton.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/sidebar/SidebarRegexToggleButton.vue b/apps/desktop/src/components/sidebar/SidebarRegexToggleButton.vue
new file mode 100644
index 0000000000..ae292b8956
--- /dev/null
+++ b/apps/desktop/src/components/sidebar/SidebarRegexToggleButton.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index 3ca0375e44..267d6a110c 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -1478,7 +1478,19 @@ function onKeydown(event: KeyboardEvent) {
@keydown.escape.prevent="cancelRename"
@click.stop
/>
-
{{ visibleLabel(node) }}
+
{{ visibleLabel(node) }}
{{ treeNodeSecondaryValue(node) }}
{
+ for (const app of mountedApps.splice(0)) app.unmount();
+ document.body.innerHTML = "";
+});
+
+describe("sidebar toolbar animations", () => {
+ it("restarts the locate pulse on every click without changing the button size", async () => {
+ const locate = vi.fn();
+ const container = await mount(
+ defineComponent({
+ setup: () => () => h(SidebarLocateButton, { label: "Locate", onLocate: locate }),
+ }),
+ );
+ const button = container.querySelector("[data-sidebar-locate-button]");
+ if (!(button instanceof HTMLButtonElement)) throw new Error("Locate button was not rendered");
+
+ expect(button.className).toContain("h-6 w-6");
+ expect(button.querySelector("[data-sidebar-locate-pulse]")).toBeNull();
+ button.click();
+ await nextTick();
+ const firstPulse = button.querySelector("[data-sidebar-locate-pulse]");
+ expect(firstPulse).not.toBeNull();
+ expect(locate).toHaveBeenCalledTimes(1);
+
+ button.click();
+ await nextTick();
+ expect(button.querySelector("[data-sidebar-locate-pulse]")).not.toBe(firstPulse);
+ expect(locate).toHaveBeenCalledTimes(2);
+ });
+
+ it("morphs the list options icon between unfiltered and filtered states", async () => {
+ const state = reactive({ filtered: false, open: false });
+ const container = await mount(
+ defineComponent({
+ setup: () => () => h(SidebarListOptionsIcon, { filtered: state.filtered, open: state.open }),
+ }),
+ );
+
+ const icon = container.querySelector("[data-sidebar-list-options-icon]");
+ expect(icon?.querySelector("[data-sidebar-unfiltered-icon]")).not.toBeNull();
+ expect(icon?.className).not.toContain("sidebar-list-options-icon--filtered");
+
+ state.filtered = true;
+ state.open = true;
+ await nextTick();
+ expect(icon?.querySelector("[data-sidebar-filtered-icon]")).not.toBeNull();
+ expect(icon?.className).toContain("sidebar-list-options-icon--filtered");
+ expect(icon?.className).toContain("sidebar-list-options-icon--open");
+
+ state.filtered = false;
+ state.open = false;
+ await nextTick();
+ expect(icon?.querySelector("[data-sidebar-unfiltered-icon]")).not.toBeNull();
+ expect(icon?.className).not.toContain("sidebar-list-options-icon--filtered");
+ });
+
+ it("replays a bold scale animation in both JavaScript regex states", async () => {
+ const state = reactive({ pressed: false, invalid: false });
+ const container = await mount(
+ defineComponent({
+ setup: () => () =>
+ h(SidebarRegexToggleButton, {
+ label: "JavaScript regex",
+ pressed: state.pressed,
+ invalid: state.invalid,
+ onToggle: () => {
+ state.pressed = !state.pressed;
+ },
+ }),
+ }),
+ );
+ const button = container.querySelector("[data-sidebar-regex-toggle]");
+ if (!(button instanceof HTMLButtonElement)) throw new Error("Regex button was not rendered");
+
+ expect(button.getAttribute("aria-pressed")).toBe("false");
+ expect(button.querySelector("[data-sidebar-regex-indicator]")).toBeNull();
+ const initialGlyph = button.querySelector("[data-sidebar-regex-glyph]");
+ button.click();
+ await nextTick();
+ expect(button.getAttribute("aria-pressed")).toBe("true");
+ expect(button.className).toContain("sidebar-regex-toggle--pressed");
+ const pressedGlyph = button.querySelector("[data-sidebar-regex-glyph]");
+ expect(pressedGlyph).not.toBe(initialGlyph);
+ expect(pressedGlyph?.className).toContain("sidebar-regex-glyph--pressed");
+ expect(pressedGlyph?.className).toContain("sidebar-regex-glyph--animated");
+
+ button.click();
+ await nextTick();
+ expect(button.getAttribute("aria-pressed")).toBe("false");
+ expect(button.className).not.toContain("sidebar-regex-toggle--pressed");
+ const releasedGlyph = button.querySelector("[data-sidebar-regex-glyph]");
+ expect(releasedGlyph).not.toBe(pressedGlyph);
+ expect(releasedGlyph?.className).not.toContain("sidebar-regex-glyph--pressed");
+ expect(releasedGlyph?.className).toContain("sidebar-regex-glyph--animated");
+ });
+});
diff --git a/apps/desktop/src/components/ui/LightDropdown.vue b/apps/desktop/src/components/ui/LightDropdown.vue
index 0d9931b90c..850005e09b 100644
--- a/apps/desktop/src/components/ui/LightDropdown.vue
+++ b/apps/desktop/src/components/ui/LightDropdown.vue
@@ -173,7 +173,9 @@ onBeforeUnmount(close);
-
+
+
+
{{ triggerLabel ?? selectedItem?.label }}
diff --git a/packages/app-tests/sidebarToolbarTooltip.test.ts b/packages/app-tests/sidebarToolbarTooltip.test.ts
index 47c74e67d3..0e03560179 100644
--- a/packages/app-tests/sidebarToolbarTooltip.test.ts
+++ b/packages/app-tests/sidebarToolbarTooltip.test.ts
@@ -5,17 +5,20 @@ import { test } from "vitest";
const connectionTreeSource = readFileSync("apps/desktop/src/components/sidebar/ConnectionTree.vue", "utf8");
const appSidebarSource = readFileSync("apps/desktop/src/components/layout/AppSidebar.vue", "utf8");
const activeConnectionFilterSource = readFileSync("apps/desktop/src/components/sidebar/ActiveConnectionFilterButton.vue", "utf8");
+const locateButtonSource = readFileSync("apps/desktop/src/components/sidebar/SidebarLocateButton.vue", "utf8");
+const regexButtonSource = readFileSync("apps/desktop/src/components/sidebar/SidebarRegexToggleButton.vue", "utf8");
const toolbarStart = connectionTreeSource.indexOf('