From 9ca4f6f4f8a6867ee132c80f57eea2ea614fd68f Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 25 Apr 2026 21:13:59 +0100 Subject: [PATCH] feat: add mobile drawer auto-hide when keyboard is active Implement optional drawer element hiding on mobile devices when the chat input is focused, providing more screen space for the on-screen keyboard. Elements smoothly animate away on focus and restore on blur. Includes new setting in Advanced Settings section and updates Anthropic SDK to 0.91.1. --- Components/ChatInput.svelte | 14 +++++++- Enums/Copy.ts | 4 +++ Helpers/ElementHelper.ts | 62 ++++++++++++++++++++++++++++++++ Services/SettingsService.ts | 6 +++- Styles/custom_styles.css | 59 +++++++++++++++++++++++++----- Views/VaultkeeperAISettingTab.ts | 18 ++++++++++ package-lock.json | 20 +++++------ package.json | 2 +- 8 files changed, 163 insertions(+), 22 deletions(-) diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index 11ebd5c..ff07811 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -24,6 +24,7 @@ import type { IPrompt } from "AIPrompts/IPrompt"; import type { SettingsService } from "Services/SettingsService"; import { ChatMode, chatModeAllowsEdits, iconForChatMode } from "Enums/ChatMode"; + import { hideDrawerElements, restoreDrawerElements } from "Helpers/ElementHelper"; export let attachments: Attachment[] = []; @@ -82,7 +83,8 @@ }); function checkStacked() { - if (textareaElement.textContent.trim() === "") { + // Mobile already uses the 'stacked' layout + if (Platform.isMobile || textareaElement.textContent.trim() === "") { stacked = false; return; } @@ -503,8 +505,17 @@ } } + function handleFocusIn() { + if (Platform.isMobile && settingsService.settings.hideDrawerElements) { + hideDrawerElements(); + } + } + function handleFocusOut() { searchStateStore.resetSearch(); + if (Platform.isMobile && settingsService.settings.hideDrawerElements) { + restoreDrawerElements(); + } } @@ -565,6 +576,7 @@ on:dragleave={handleDragLeave} on:click={handleCursorPositionChange} on:keyup={handleCursorPositionChange} + on:focus={handleFocusIn} on:focusout={handleFocusOut} data-placeholder={inputPlaceholder} role="textbox" diff --git a/Enums/Copy.ts b/Enums/Copy.ts index d4f678f..a0e3aac 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -84,6 +84,10 @@ export enum Copy { TooltipLearnMoreFileMonitoring = "Learn more in Plugin Guide", TooltipAccessMemories = "View Memories", + SettingAdvancedSettings = "Advanced Settings", + SettingHideDrawerElements = "Hide Drawer Elements", + SettingHideDrawerElementsDesc = "Hide side drawer elements on mobile when typing. This provides more space when an on screen keyboard is used. The elements will reappear when the chat input is unfocused.", + SettingQuickActions = "Quick Actions", SettingEnableContextMenuActions = "Enable Context Menu Actions", SettingEnableContextMenuActionsDesc = "Show quick actions in the right-click editor context menu.", diff --git a/Helpers/ElementHelper.ts b/Helpers/ElementHelper.ts index 853c291..5a6813e 100644 --- a/Helpers/ElementHelper.ts +++ b/Helpers/ElementHelper.ts @@ -17,4 +17,66 @@ export function setElementIcon(element: HTMLDivElement, iconName: string) { } } }; +} + +export function hideDrawerElements() { + for (const element of getDrawerElements()) { + if ("vaultKeeperHidden" in element.dataset) continue; + + const computed = getComputedStyle(element); + element.dataset.vaultKeeperHidden = String(element.scrollHeight); + element.dataset.vaultKeeperPaddingTop = computed.paddingTop; + element.dataset.vaultKeeperPaddingBottom = computed.paddingBottom; + + element.setCssProps({ + "--vaultKeeper-drawer-height": element.scrollHeight + "px", + "--vaultKeeper-drawer-padding-top": computed.paddingTop, + "--vaultKeeper-drawer-padding-bottom": computed.paddingBottom, + }); + element.addClass("vaultKeeper-drawer-hiding"); + + requestAnimationFrame(() => { + element.removeClass("vaultKeeper-drawer-hiding"); + element.addClass("vaultKeeper-drawer-collapsed"); + }); + + element.addEventListener("transitionend", () => { + element.removeClass("vaultKeeper-drawer-collapsed"); + element.addClass("vaultKeeper-drawer-hidden"); + }, { once: true }); + } +} + +export function restoreDrawerElements() { + for (const element of getDrawerElements()) { + if (!("vaultKeeperHidden" in element.dataset)) continue; + + const targetHeight = element.dataset.vaultKeeperHidden ?? "0"; + const paddingTop = element.dataset.vaultKeeperPaddingTop ?? "0"; + const paddingBottom = element.dataset.vaultKeeperPaddingBottom ?? "0"; + delete element.dataset.vaultKeeperHidden; + delete element.dataset.vaultKeeperPaddingTop; + delete element.dataset.vaultKeeperPaddingBottom; + + element.setCssProps({ + "--vaultKeeper-drawer-height": targetHeight + "px", + "--vaultKeeper-drawer-padding-top": paddingTop, + "--vaultKeeper-drawer-padding-bottom": paddingBottom, + }); + element.removeClass("vaultKeeper-drawer-hidden"); + element.addClass("vaultKeeper-drawer-restoring"); + + requestAnimationFrame(() => { + element.removeClass("vaultKeeper-drawer-restoring"); + element.addClass("vaultKeeper-drawer-expanding"); + }); + + element.addEventListener("transitionend", () => { + element.removeClass("vaultKeeper-drawer-expanding"); + }, { once: true }); + } +} + +function getDrawerElements(): NodeListOf { + return activeDocument.querySelectorAll(".workspace-drawer-header, .workspace-drawer-tab-options"); } \ No newline at end of file diff --git a/Services/SettingsService.ts b/Services/SettingsService.ts index dfae81c..c7018f6 100644 --- a/Services/SettingsService.ts +++ b/Services/SettingsService.ts @@ -30,7 +30,9 @@ const DEFAULT_SETTINGS: IVaultkeeperAISettings = { enableWebViewer: false, enableContextMenuActions: true, - enableToolbarActions: true + enableToolbarActions: true, + + hideDrawerElements: true } export interface IVaultkeeperAISettings { @@ -61,6 +63,8 @@ export interface IVaultkeeperAISettings { enableContextMenuActions: boolean; enableToolbarActions: boolean; + + hideDrawerElements: boolean; } export class SettingsService { diff --git a/Styles/custom_styles.css b/Styles/custom_styles.css index 155d562..93ba5f8 100644 --- a/Styles/custom_styles.css +++ b/Styles/custom_styles.css @@ -1,12 +1,3 @@ -/* ============================== */ -/* Mobile Drawer Full Width */ -/* ============================== */ - -body:not(.is-tablet) .workspace-drawer.mod-right { - max-width: 100%; - width: 100%; -} - /* ============================== */ /* Main View Customization */ /* ============================== */ @@ -22,6 +13,56 @@ body:not(.is-tablet) .workspace-drawer.mod-right { padding-top: 0; } +/* ============================== */ +/* Mobile Drawer Full Width */ +/* ============================== */ + +body:not(.is-tablet) .workspace-drawer.mod-right { + max-width: 100%; + width: 100%; +} + +/* ============================== */ +/* Drawer element collapse animation */ +/* ============================== */ + +.vaultKeeper-drawer-hiding { + overflow: hidden; + height: var(--vaultKeeper-drawer-height); + padding-top: var(--vaultKeeper-drawer-padding-top); + padding-bottom: var(--vaultKeeper-drawer-padding-bottom); + transition: height 0.25s ease, padding-top 0.25s ease, padding-bottom 0.25s ease; +} + +.vaultKeeper-drawer-collapsed { + overflow: hidden; + height: 0; + padding-top: 0; + padding-bottom: 0; + transition: height 0.25s ease, padding-top 0.25s ease, padding-bottom 0.25s ease; +} + +.vaultKeeper-drawer-hidden { + display: none; +} + +.vaultKeeper-drawer-restoring { + overflow: hidden; + display: block; + height: 0; + padding-top: 0; + padding-bottom: 0; + transition: height 0.25s ease, padding-top 0.25s ease, padding-bottom 0.25s ease; +} + +.vaultKeeper-drawer-expanding { + overflow: hidden; + height: var(--vaultKeeper-drawer-height); + padding-top: var(--vaultKeeper-drawer-padding-top); + padding-bottom: var(--vaultKeeper-drawer-padding-bottom); + transition: height 0.25s ease, padding-top 0.25s ease, padding-bottom 0.25s ease; +} + /* ============================== */ /* Quick Actions Notice */ /* ============================== */ diff --git a/Views/VaultkeeperAISettingTab.ts b/Views/VaultkeeperAISettingTab.ts index 0cfb2e4..c09b42e 100644 --- a/Views/VaultkeeperAISettingTab.ts +++ b/Views/VaultkeeperAISettingTab.ts @@ -303,6 +303,24 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { ); }); }); + + /* Advanced Settings Header */ + new Setting(containerEl) + .setHeading() + .setName(Copy.SettingAdvancedSettings); + + /* Hide Drawer Elements */ + new Setting(containerEl) + .setName(Copy.SettingHideDrawerElements) + .setDesc(Copy.SettingHideDrawerElementsDesc) + .addToggle(toggle => { + toggle + .setValue(this.settingsService.settings.hideDrawerElements) + .onChange(async (value) => { + this.settingsService.settings.hideDrawerElements = value; + await this.settingsService.saveSettings(); + }); + }); } private populateModelDropdown(dropdown: DropdownComponent, providerFilter?: AIProvider): void { diff --git a/package-lock.json b/package-lock.json index 2f0d4ab..aae5442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.91.0", + "@anthropic-ai/sdk": "^0.91.1", "@google/genai": "^1.50.1", "@shikijs/rehype": "^4.0.2", "core-js": "^3.49.0", @@ -64,9 +64,9 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.91.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.0.tgz", - "integrity": "sha512-hybd/DOI3ujG4gZyqqcWnSekYxkdjr1JbZYqP2Lb4AmcsU6HCTHSrTOgqedPSsQAruBVucHNAoD1vTQnpPzedw==", + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -4196,9 +4196,9 @@ } }, "node_modules/eslint-plugin-json-schema-validator/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -6788,9 +6788,9 @@ } }, "node_modules/json-schema-migrate/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 50900a1..d93808e 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "vitest": "^4.1.5" }, "dependencies": { - "@anthropic-ai/sdk": "^0.91.0", + "@anthropic-ai/sdk": "^0.91.1", "@google/genai": "^1.50.1", "@shikijs/rehype": "^4.0.2", "core-js": "^3.49.0",