From afaa530f8bd97d8db320de6e8f764d79d8fb9e00 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sun, 5 Jul 2026 19:59:07 +0100 Subject: [PATCH] feat: add mobile suggestion button and force-focus parameter - Add "Suggest" button to mobile diff controls with three-column layout - Replace focusInput's onMobile param with force param for explicit control - Scope diff mobile button styles to prevent conflicts - Enable focus on chat input when suggestion button clicked --- Components/ChatInput.svelte | 6 +++--- Components/ChatWindow.svelte | 4 ++-- Styles/diff2html_styles.css | 20 ++++++++++++++------ Views/DiffView.ts | 18 ++++++++++++++++-- Views/MainView.ts | 2 +- __tests__/setup.ts | 6 +++--- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index 9fee699..77189f9 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -115,9 +115,9 @@ } } - export function focusInput(onMobile: boolean = false) { - // don't focus on mobile, it's annoying - if (onMobile || !Platform.isMobile) { + export function focusInput(force: boolean = false) { + // Generally don't focus on mobile, it's annoying + if (force || !Platform.isMobile) { tick().then(() => { textareaElement?.focus(); }); diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 16e6328..fa7a671 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -43,8 +43,8 @@ let currentThought: string | null = null; - export function focusInput() { - chatInput?.focusInput(); + export function focusInput(force: boolean = false) { + chatInput?.focusInput(force); } export function resetChatArea() { diff --git a/Styles/diff2html_styles.css b/Styles/diff2html_styles.css index 6f695d8..5043d28 100644 --- a/Styles/diff2html_styles.css +++ b/Styles/diff2html_styles.css @@ -118,7 +118,7 @@ .diff-view-mobile .diff-mobile-controls { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; gap: var(--size-4-2); position: absolute; bottom: 0; @@ -128,7 +128,7 @@ z-index: 10; } -.diff-mobile-button { +.diff-mobile-controls .diff-mobile-button { min-height: 44px; font-size: var(--font-ui-medium); font-weight: var(--font-semibold); @@ -138,7 +138,7 @@ color: var(--text-on-accent); } -.diff-mobile-accept { +.diff-mobile-controls .diff-mobile-accept { background-color: color-mix( in srgb, var(--color-green) 75%, @@ -146,11 +146,19 @@ ); } -.diff-mobile-accept:active { +.diff-mobile-controls .diff-mobile-accept:active { background-color: var(--color-green); } -.diff-mobile-reject { +.diff-mobile-controls .diff-mobile-suggest { + background-color: var(--interactive-accent); +} + +.diff-mobile-controls .diff-mobile-suggest:active { + background-color: var(--interactive-accent-hover); +} + +.diff-mobile-controls .diff-mobile-reject { background-color: color-mix( in srgb, var(--color-red) 75%, @@ -158,7 +166,7 @@ ); } -.diff-mobile-reject:active { +.diff-mobile-controls .diff-mobile-reject:active { background-color: var(--color-red); } diff --git a/Views/DiffView.ts b/Views/DiffView.ts index 66c3c2f..ecedcf0 100644 --- a/Views/DiffView.ts +++ b/Views/DiffView.ts @@ -5,7 +5,7 @@ import { Resolve } from "Services/DependencyService"; import type { EventService } from "Services/EventService"; import type { DiffService } from "Services/DiffService"; import { Services } from "Services/Services"; -import { VIEW_TYPE_MAIN } from "./MainView"; +import { VIEW_TYPE_MAIN, type MainView } from "./MainView"; import { tick } from "svelte"; export const VIEW_TYPE_DIFF = 'vaultkeeper-ai-diff-view'; @@ -115,6 +115,12 @@ export class DiffView extends ItemView { }); acceptButton.setAttribute('aria-label', 'Accept changes'); + const suggestButton = container.createEl('button', { + cls: 'diff-mobile-button diff-mobile-suggest', + text: 'Suggest' + }); + suggestButton.setAttribute('aria-label', 'Make a suggestion'); + const rejectButton = container.createEl('button', { cls: 'diff-mobile-button diff-mobile-reject', text: 'Reject' @@ -126,6 +132,10 @@ export class DiffView extends ItemView { await this.refocusMainView(); }); + this.registerDomEvent(suggestButton, 'click', async () => { + await this.refocusMainView(true); + }); + this.registerDomEvent(rejectButton, 'click', async () => { this.diffService.onReject(); await this.refocusMainView(); @@ -134,7 +144,7 @@ export class DiffView extends ItemView { return container; } - private async refocusMainView(): Promise { + private async refocusMainView(focusChatInput: boolean = false): Promise { await tick().then(async () => { const { workspace } = this.app; const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN); @@ -142,6 +152,10 @@ export class DiffView extends ItemView { if (leaves.length > 0) { await workspace.revealLeaf(leaves[0]); workspace.setActiveLeaf(leaves[0], { focus: true }); + + if (focusChatInput) { + (leaves[0].view as MainView).input?.focusInput(true); + } } }); } diff --git a/Views/MainView.ts b/Views/MainView.ts index 3bd8f80..aacea29 100644 --- a/Views/MainView.ts +++ b/Views/MainView.ts @@ -9,7 +9,7 @@ import { Services } from 'Services/Services'; export const VIEW_TYPE_MAIN = 'vaultkeeper-ai-main-view'; interface ChatWindowComponent { - focusInput: () => void; + focusInput: (force?: boolean) => void; resetChatArea: () => void; } diff --git a/__tests__/setup.ts b/__tests__/setup.ts index 5d45d56..6be2705 100644 --- a/__tests__/setup.ts +++ b/__tests__/setup.ts @@ -92,7 +92,7 @@ if (typeof HTMLElement !== 'undefined') { options?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[K]) => void ): HTMLElementTagNameMap[K] { - const el = document.createElement(tag); + const el = document.createElement(tag) as HTMLElementTagNameMap[K]; applyElementInfo(el, options); if (this instanceof HTMLElement && el.parentNode !== this) { this.appendChild(el); @@ -101,9 +101,9 @@ if (typeof HTMLElement !== 'undefined') { return el; } - HTMLElement.prototype.createEl = function(tag: string, options?: string | DomElementInfo, callback?: (el: HTMLElement) => void) { + HTMLElement.prototype.createEl = function(this: HTMLElement, tag: string, options?: string | DomElementInfo, callback?: (el: HTMLElement) => void) { return createElImpl.call(this, tag as keyof HTMLElementTagNameMap, options, callback as any); - }; + } as HTMLElement['createEl']; HTMLElement.prototype.createDiv = function(options?: string | DomElementInfo, callback?: (el: HTMLDivElement) => void) { return this.createEl('div', options, callback as any); };