From 6c0afcfc8eba0a735bfd7fc7796bc302d3336939 Mon Sep 17 00:00:00 2001 From: Galen Green Date: Fri, 27 Feb 2026 16:49:19 +0100 Subject: [PATCH 1/3] feat: polish sidebar shell interactions - Set sidebar closed by default on launch - Adjust sidebar geometry for open and hover states - Increase new tab button spacing in editor tabs - Remove outdated project status section from README - Bump app version to 0.6.0 across npm and Tauri manifests --- README.md | 17 -- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src/modules/editor/ui/EditorTabs.vue | 2 +- .../runtime/useSidebarInteraction.ts | 2 +- src/modules/workspace/ui/Sidebar.vue | 201 +++++++++--------- 8 files changed, 109 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index d58da27..bec2f29 100644 --- a/README.md +++ b/README.md @@ -28,23 +28,6 @@ npm run tauri dev npm run build ``` -## Documentation - -- [Development Progress](docs/progress.md) - Current status and completed features -- [Architecture](docs/architecture.md) - System design and technical overview -- [Roadmap](docs/roadmap.md) - Development phases and milestones -- [Requirements](docs/requirements.md) - Feature specifications and priorities - -## Project Status - -**Current Version**: 0.1.0 (Phase 1 - In Progress) - -✅ **Completed**: Foundation editor with markdown support, file management, multi-tab support -🚧 **In Progress**: File browser sidebar, folder operations, quick switcher -⏳ **Planned**: P2P collaboration, syntax highlighting, advanced editor features - -See [Development Progress](docs/progress.md) for details on what's been built. - ## License [MIT License](LICENSE) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3e84a36..17d7566 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kea", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kea", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@codemirror/autocomplete": "^6.18.6", "@codemirror/commands": "^6.9.1", diff --git a/package.json b/package.json index 8c16734..20dc842 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kea", "private": true, - "version": "0.5.0", + "version": "0.6.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f66ab07..db83db3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "Kea" -version = "0.5.0" +version = "0.6.0" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ec93760..787d020 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Kea" -version = "0.5.0" +version = "0.6.0" description = "The Better Markdown Writer" authors = ["Galen Green"] edition = "2021" diff --git a/src/modules/editor/ui/EditorTabs.vue b/src/modules/editor/ui/EditorTabs.vue index 5e27268..ea87656 100644 --- a/src/modules/editor/ui/EditorTabs.vue +++ b/src/modules/editor/ui/EditorTabs.vue @@ -751,7 +751,7 @@ onBeforeUnmount(() => { color: var(--tt-gray-light-600); cursor: pointer; flex-shrink: 0; - margin-left: 2px; + margin-left: 7px; } .new-tab-btn:hover { diff --git a/src/modules/workspace/runtime/useSidebarInteraction.ts b/src/modules/workspace/runtime/useSidebarInteraction.ts index 05e505a..795f6fc 100644 --- a/src/modules/workspace/runtime/useSidebarInteraction.ts +++ b/src/modules/workspace/runtime/useSidebarInteraction.ts @@ -1,7 +1,7 @@ import { onUnmounted, ref } from 'vue' export function useSidebarInteraction() { - const sidebarOpen = ref(true) + const sidebarOpen = ref(false) const sidebarHovering = ref(false) const hoverDisabled = ref(false) diff --git a/src/modules/workspace/ui/Sidebar.vue b/src/modules/workspace/ui/Sidebar.vue index 3824c47..6279787 100644 --- a/src/modules/workspace/ui/Sidebar.vue +++ b/src/modules/workspace/ui/Sidebar.vue @@ -1,12 +1,12 @@ From b361a7b0c7f2ed537ecb95ad92c5076e11025fdc Mon Sep 17 00:00:00 2001 From: Galen Green Date: Fri, 27 Feb 2026 16:52:18 +0100 Subject: [PATCH 2/3] test: update sidebar interaction toggle expectations - Open then close in the interaction test before asserting closed-state behaviour - Keep hover-disable assertions aligned with the sidebar default closed state --- tests/unit/use-sidebar-interaction.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/use-sidebar-interaction.test.ts b/tests/unit/use-sidebar-interaction.test.ts index 98a0c8e..8ada841 100644 --- a/tests/unit/use-sidebar-interaction.test.ts +++ b/tests/unit/use-sidebar-interaction.test.ts @@ -28,6 +28,9 @@ describe('useSidebarInteraction', () => { vm.handleSidebarHover(true) expect(vm.sidebarHovering).toBe(true) + vm.toggleSidebar() + expect(vm.sidebarOpen).toBe(true) + vm.toggleSidebar() expect(vm.sidebarOpen).toBe(false) expect(vm.sidebarHovering).toBe(false) From 89217d15c85e215637ae09a237682dd205962f06 Mon Sep 17 00:00:00 2001 From: Galen Green Date: Fri, 27 Feb 2026 17:30:52 +0100 Subject: [PATCH 3/3] test: increase editor tabs drag coverage - Add measured and fallback drag geometry scenarios for tab reordering - Cover non-primary click, close-button mousedown, and early mouseup guard paths - Assert drag UI state helpers and click-suppression behaviour after reorder --- tests/unit/editor-tabs.test.ts | 294 ++++++++++++++++++++++++++++++++- 1 file changed, 286 insertions(+), 8 deletions(-) diff --git a/tests/unit/editor-tabs.test.ts b/tests/unit/editor-tabs.test.ts index e52dac9..378f711 100644 --- a/tests/unit/editor-tabs.test.ts +++ b/tests/unit/editor-tabs.test.ts @@ -16,6 +16,56 @@ function makeTab(id: string, name: string): OpenDocument { } } +function createRect(left: number, width: number, top = 100, height = 28): DOMRect { + return { + x: left, + y: top, + left, + top, + width, + height, + right: left + width, + bottom: top + height, + toJSON: () => ({}), + } as DOMRect +} + +function setRect(element: Element, rect: DOMRect) { + Object.defineProperty(element, 'getBoundingClientRect', { + configurable: true, + value: () => rect, + }) +} + +function setOffsets(element: Element, left: number, width: number) { + Object.defineProperty(element, 'offsetLeft', { + configurable: true, + get: () => left, + }) + + Object.defineProperty(element, 'offsetWidth', { + configurable: true, + get: () => width, + }) +} + +function applyMeasuredGeometry(wrapper: ReturnType) { + const tabsContainer = wrapper.get('.tabs-container').element + const tabsList = wrapper.get('.tabs-list').element + const tabs = wrapper.findAll('.tab') + + setRect(tabsContainer, createRect(10, 300, 90, 40)) + setRect(tabsList, createRect(10, 260, 100, 30)) + + tabs.forEach((tab, index) => { + const left = 20 + (index * 110) + const width = 100 + + setRect(tab.element, createRect(left, width)) + setOffsets(tab.element, left, width) + }) +} + describe('EditorTabs', () => { beforeEach(() => { setActivePinia(createPinia()) @@ -59,26 +109,65 @@ describe('EditorTabs', () => { }) it('reorders tabs while dragging', async () => { + vi.useFakeTimers() const documentStore = useDocumentStore() - documentStore.openDocuments = [makeTab('tab-1', 'first.md'), makeTab('tab-2', 'second.md')] + const first = makeTab('tab-1', 'first.md') + const second = makeTab('tab-2', 'second.md') + second.isDirty = true + documentStore.openDocuments = [first, second] documentStore.activeDocumentId = 'tab-1' const reorderSpy = vi.spyOn(documentStore, 'reorderTabs') - const wrapper = mount(EditorTabs) - const tabs = wrapper.findAll('.tab') + const setActiveSpy = vi.spyOn(documentStore, 'setActiveDocument') - Object.defineProperty(document, 'elementFromPoint', { - configurable: true, - writable: true, - value: vi.fn(() => tabs[1].element), + const frameCallbacks: FrameRequestCallback[] = [] + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { + frameCallbacks.push(callback) + return frameCallbacks.length }) + const cancelAnimationFrameSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => 0) + + const wrapper = mount(EditorTabs) + applyMeasuredGeometry(wrapper) + + const tabs = wrapper.findAll('.tab') await tabs[0].trigger('mousedown', { button: 0, clientX: 20, clientY: 20 }) document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, - clientX: 40, + clientX: 22, + clientY: 22, + })) + + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 380, + clientY: 40, + })) + + frameCallbacks[0]?.(16) + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.drop-indicator').exists()).toBe(true) + expect(wrapper.find('.tab.is-drag-ghost').exists()).toBe(true) + expect(wrapper.find('.tab.is-dragging').exists()).toBe(true) + expect(wrapper.find('.dirty-indicator').exists()).toBe(true) + + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: -100, + clientY: 40, + })) + + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 380, clientY: 40, })) @@ -93,5 +182,194 @@ describe('EditorTabs', () => { expect(reorderSpy).toHaveBeenCalledWith(0, 1) expect(documentStore.openDocuments.map(doc => doc.id)).toEqual(['tab-2', 'tab-1']) + + await wrapper.findAll('.tab')[0].trigger('click') + expect(setActiveSpy).not.toHaveBeenCalled() + + vi.runAllTimers() + await wrapper.findAll('.tab')[0].trigger('click') + expect(setActiveSpy).toHaveBeenCalled() + + wrapper.unmount() + expect(cancelAnimationFrameSpy).toHaveBeenCalled() + }) + + it('does not begin drag for non-primary clicks', async () => { + const documentStore = useDocumentStore() + documentStore.openDocuments = [makeTab('tab-1', 'first.md'), makeTab('tab-2', 'second.md')] + documentStore.activeDocumentId = 'tab-1' + + const reorderSpy = vi.spyOn(documentStore, 'reorderTabs') + const wrapper = mount(EditorTabs) + + await wrapper.find('.tab').trigger('mousedown', { button: 2, clientX: 10, clientY: 10 }) + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 40, + clientY: 40, + })) + document.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + clientX: 40, + clientY: 40, + })) + + expect(reorderSpy).not.toHaveBeenCalled() + }) + + it('resets drag state when mouseup happens before drag threshold', async () => { + const documentStore = useDocumentStore() + documentStore.openDocuments = [makeTab('tab-1', 'first.md'), makeTab('tab-2', 'second.md')] + documentStore.activeDocumentId = 'tab-1' + + const reorderSpy = vi.spyOn(documentStore, 'reorderTabs') + const wrapper = mount(EditorTabs) + + await wrapper.find('.tab').trigger('mousedown', { button: 0, clientX: 20, clientY: 20 }) + document.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + clientX: 20, + clientY: 20, + })) + + expect(reorderSpy).not.toHaveBeenCalled() + expect(wrapper.find('.tab.is-dragging').exists()).toBe(false) + }) + + it('does not start drag when pressing the close button', async () => { + const documentStore = useDocumentStore() + documentStore.openDocuments = [makeTab('tab-1', 'first.md'), makeTab('tab-2', 'second.md')] + documentStore.activeDocumentId = 'tab-1' + + const reorderSpy = vi.spyOn(documentStore, 'reorderTabs') + const closeSpy = vi.spyOn(documentStore, 'closeDocument').mockResolvedValue(true) + + const wrapper = mount(EditorTabs) + + await wrapper.find('.close-btn').trigger('mousedown', { button: 0, clientX: 10, clientY: 10 }) + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 40, + clientY: 40, + })) + document.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + clientX: 40, + clientY: 40, + })) + + await wrapper.find('.close-btn').trigger('click') + + expect(reorderSpy).not.toHaveBeenCalled() + expect(closeSpy).toHaveBeenCalledWith('tab-1') + }) + + it('skips reorder when drag ends without a valid drop target', async () => { + const documentStore = useDocumentStore() + documentStore.openDocuments = [makeTab('tab-1', 'first.md'), makeTab('tab-2', 'second.md')] + documentStore.activeDocumentId = 'tab-1' + + const reorderSpy = vi.spyOn(documentStore, 'reorderTabs') + const wrapper = mount(EditorTabs) + const tabs = wrapper.findAll('.tab') + + setRect(wrapper.get('.tabs-container').element, createRect(10, 0, 90, 40)) + setRect(wrapper.get('.tabs-list').element, createRect(10, 0, 100, 30)) + tabs.forEach((tab) => { + setRect(tab.element, createRect(20, 0)) + setOffsets(tab.element, 20, 0) + }) + + const elementFromPoint = vi.fn(() => null) + Object.defineProperty(document, 'elementFromPoint', { + configurable: true, + value: elementFromPoint, + }) + + await tabs[0].trigger('mousedown', { button: 0, clientX: 20, clientY: 20 }) + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 80, + clientY: 40, + })) + document.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + clientX: 80, + clientY: 40, + })) + + expect(elementFromPoint).toHaveBeenCalled() + expect(reorderSpy).not.toHaveBeenCalled() + }) + + it('uses fallback tab targeting when geometry is unavailable', async () => { + const documentStore = useDocumentStore() + documentStore.openDocuments = [makeTab('tab-1', 'first.md'), makeTab('tab-2', 'second.md')] + documentStore.activeDocumentId = 'tab-1' + + const reorderSpy = vi.spyOn(documentStore, 'reorderTabs') + const wrapper = mount(EditorTabs) + const tabs = wrapper.findAll('.tab') + + setRect(wrapper.get('.tabs-container').element, createRect(10, 0, 90, 40)) + setRect(wrapper.get('.tabs-list').element, createRect(10, 0, 100, 30)) + tabs.forEach((tab) => { + setRect(tab.element, createRect(20, 0)) + setOffsets(tab.element, 20, 0) + }) + + const elementFromPoint = vi.fn() + .mockReturnValueOnce(tabs[0].element) + .mockReturnValueOnce(tabs[1].element) + + Object.defineProperty(document, 'elementFromPoint', { + configurable: true, + value: elementFromPoint, + }) + + await tabs[0].trigger('mousedown', { button: 0, clientX: 20, clientY: 20 }) + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 60, + clientY: 40, + })) + document.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + clientX: 120, + clientY: 40, + })) + document.dispatchEvent(new MouseEvent('mouseup', { + bubbles: true, + cancelable: true, + clientX: 120, + clientY: 40, + })) + + expect(elementFromPoint).toHaveBeenCalledTimes(2) + expect(reorderSpy).toHaveBeenCalledWith(0, 1) + }) + + it('returns undefined drag styles when no drag UI is active', () => { + const documentStore = useDocumentStore() + documentStore.openDocuments = [makeTab('tab-1', 'first.md')] + documentStore.activeDocumentId = 'tab-1' + + const wrapper = mount(EditorTabs) + const vm = wrapper.vm as unknown as { + getTabStyle: (tabId: string) => Record | undefined + getDropIndicatorStyle: () => Record | undefined + } + + expect(vm.getTabStyle('tab-1')).toBeUndefined() + expect(vm.getDropIndicatorStyle()).toBeUndefined() }) })