Skip to content

Commit 0e56611

Browse files
feat: improve three-panel resize UX with interact.js (#707)
* refactor: replace APP_DEFAULTS with LAYOUT_DEFAULTS, all sizes in pixels - Single source of truth for layout sizes in constants.ts - Store type: twoPanel changed from number[] to number (px) - tagsListHeight migration from percentage to pixels * feat: add useResizeHandle composable wrapping interact.js Supports horizontal and vertical drag with cursor, selection blocking, and data-resizing attribute for active state styling. * refactor: rewrite ThreeColumn layout with interact.js Unified 2-panel and 3-panel modes via flex layout. Removed reka-ui Splitter dependency. All sizes from LAYOUT_DEFAULTS. * refactor: simplify CodeSpaceLayout and NotesSpace consumers Removed percentage-based twoPanel logic. Both now use pixel-based store values with LAYOUT_DEFAULTS fallbacks via ThreeColumn. * refactor: replace reka-ui vertical split with interact.js in Library Tags panel now uses pixel-based height with useResizeHandle composable instead of reka-ui SplitterGroup percentages. * refactor: replace reka-ui vertical split with interact.js in NotesSidebarFolders Same pattern as Library.vue — tags panel uses pixel-based height with useResizeHandle composable. * fix: use lockAxis/startAxis instead of deprecated axis in interact.js
1 parent 3c24493 commit 0e56611

10 files changed

Lines changed: 351 additions & 256 deletions

File tree

src/main/store/constants.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { EditorSettings, NotesEditorSettings } from './types'
22

3-
export const APP_DEFAULTS = {
4-
sizes: {
5-
sidebar: 180,
6-
snippetList: 250,
7-
tagsList: 50, // в %
8-
},
3+
export const LAYOUT_DEFAULTS = {
4+
sidebar: { width: 200, min: 120 },
5+
list: { width: 300, min: 150 },
6+
editor: { min: 300 },
7+
tags: { height: 200, min: 80 },
98
}
109

1110
export const EDITOR_DEFAULTS: EditorSettings = {

src/main/store/module/app.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
SpaceLayoutMode,
77
} from '../types'
88
import Store from 'electron-store'
9-
import { APP_DEFAULTS } from '../constants'
9+
import { LAYOUT_DEFAULTS } from '../constants'
1010
import {
1111
asRecord,
1212
isRecord,
@@ -28,15 +28,15 @@ const APP_STORE_DEFAULTS: AppStore = {
2828
selection: {},
2929
layout: {
3030
mode: 'all-panels',
31-
tagsListHeight: APP_DEFAULTS.sizes.tagsList,
31+
tagsListHeight: LAYOUT_DEFAULTS.tags.height,
3232
},
3333
},
3434
notes: {
3535
selection: {},
3636
editorMode: 'livePreview',
3737
layout: {
3838
mode: 'all-panels',
39-
tagsListHeight: APP_DEFAULTS.sizes.tagsList,
39+
tagsListHeight: LAYOUT_DEFAULTS.tags.height,
4040
},
4141
},
4242
notifications: {
@@ -139,21 +139,25 @@ function sanitizeAppStore(value: unknown): AppStore {
139139
['all-panels', 'list-editor', 'editor-only'] as const,
140140
getLegacyCodeLayoutMode(asRecord(source.state)),
141141
),
142-
tagsListHeight: readNumber(
143-
codeLayoutSource,
144-
'tagsListHeight',
145-
readNumber(
146-
legacySizes,
142+
tagsListHeight: (() => {
143+
const raw = readNumber(
144+
codeLayoutSource,
147145
'tagsListHeight',
148-
APP_STORE_DEFAULTS.code.layout.tagsListHeight,
149-
),
150-
),
146+
readNumber(
147+
legacySizes,
148+
'tagsListHeight',
149+
LAYOUT_DEFAULTS.tags.height,
150+
),
151+
)
152+
return raw < 100 ? LAYOUT_DEFAULTS.tags.height : raw
153+
})(),
151154
threePanel:
152155
readOptionalNumberArray(codeLayoutSource, 'threePanel')
153156
|| readOptionalNumberArray(legacySizes, 'layout'),
154157
twoPanel:
155-
readOptionalNumberArray(codeLayoutSource, 'twoPanel')
156-
|| readOptionalNumberArray(legacySizes, 'codeListLayout'),
158+
readOptionalNumber(codeLayoutSource, 'twoPanel')
159+
?? readOptionalNumber(legacySizes, 'codeListLayout')
160+
?? undefined,
157161
},
158162
},
159163
notes: {
@@ -180,21 +184,25 @@ function sanitizeAppStore(value: unknown): AppStore {
180184
['all-panels', 'list-editor', 'editor-only'] as const,
181185
getLegacyNotesLayoutMode(asRecord(source.notesState)),
182186
),
183-
tagsListHeight: readNumber(
184-
notesLayoutSource,
185-
'tagsListHeight',
186-
readNumber(
187-
legacySizes,
188-
'notesTagsListHeight',
189-
APP_STORE_DEFAULTS.notes.layout.tagsListHeight,
190-
),
191-
),
187+
tagsListHeight: (() => {
188+
const raw = readNumber(
189+
notesLayoutSource,
190+
'tagsListHeight',
191+
readNumber(
192+
legacySizes,
193+
'notesTagsListHeight',
194+
LAYOUT_DEFAULTS.tags.height,
195+
),
196+
)
197+
return raw < 100 ? LAYOUT_DEFAULTS.tags.height : raw
198+
})(),
192199
threePanel:
193200
readOptionalNumberArray(notesLayoutSource, 'threePanel')
194201
|| readOptionalNumberArray(legacySizes, 'notesLayout'),
195202
twoPanel:
196-
readOptionalNumberArray(notesLayoutSource, 'twoPanel')
197-
|| readOptionalNumberArray(legacySizes, 'notesLayoutWithoutSidebar'),
203+
readOptionalNumber(notesLayoutSource, 'twoPanel')
204+
?? readOptionalNumber(legacySizes, 'notesLayoutWithoutSidebar')
205+
?? undefined,
198206
},
199207
},
200208
notifications: {

src/main/store/types/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface AppStore {
2929
mode: SpaceLayoutMode
3030
tagsListHeight: number
3131
threePanel?: number[]
32-
twoPanel?: number[]
32+
twoPanel?: number
3333
}
3434
}
3535
notes: {
@@ -39,7 +39,7 @@ export interface AppStore {
3939
mode: SpaceLayoutMode
4040
tagsListHeight: number
4141
threePanel?: number[]
42-
twoPanel?: number[]
42+
twoPanel?: number
4343
}
4444
}
4545
notifications: {
Lines changed: 31 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,51 @@
11
<script setup lang="ts">
2-
import * as Resizable from '@/components/ui/shadcn/resizable'
32
import { useApp } from '@/composables'
43
import { getCodePanels } from '@/composables/layoutModes'
54
import { store } from '@/electron'
65
76
const { codeLayoutMode } = useApp()
87
9-
const storedThreePanelLayout = store.app.get('code.layout.threePanel') as
10-
| number[]
11-
| undefined
12-
const storedTwoPanelLayout = store.app.get('code.layout.twoPanel') as
8+
const panels = computed(() => getCodePanels(codeLayoutMode.value))
9+
10+
const storedThreePanel = store.app.get('code.layout.threePanel') as
1311
| number[]
1412
| undefined
15-
const defaultThreePanelLayout = storedThreePanelLayout || [15, 20, 65]
16-
const defaultTwoPanelLayout = storedTwoPanelLayout || [35, 65]
17-
const panels = computed(() => getCodePanels(codeLayoutMode.value))
1813
19-
function onLayout(layout: number[]) {
20-
if (layout.length === 3) {
21-
store.app.set('code.layout.threePanel', layout)
22-
return
23-
}
14+
const sidebarWidth
15+
= storedThreePanel?.length === 2 ? storedThreePanel[0] : undefined
16+
const listWidth = (() => {
17+
if (storedThreePanel?.length === 2)
18+
return storedThreePanel[1]
19+
const twoPanel = store.app.get('code.layout.twoPanel') as number | undefined
20+
return twoPanel ?? undefined
21+
})()
2422
25-
if (layout.length === 2) {
26-
store.app.set('code.layout.twoPanel', layout)
27-
}
23+
function onResizeEnd(sw: number, lw: number) {
24+
store.app.set('code.layout.threePanel', [sw, lw])
25+
}
26+
27+
function onTwoPanelResize(lw: number) {
28+
store.app.set('code.layout.twoPanel', lw)
2829
}
2930
</script>
3031

3132
<template>
32-
<div
33-
v-if="!panels.showList"
34-
class="h-screen"
35-
>
36-
<Editor />
37-
</div>
38-
<Resizable.ResizablePanelGroup
39-
v-else-if="!panels.showSidebar"
40-
direction="horizontal"
41-
class="h-screen"
42-
@layout="onLayout"
43-
>
44-
<Resizable.ResizablePanel
45-
:default-size="defaultTwoPanelLayout[0]"
46-
:min-size="15"
47-
>
48-
<SnippetList />
49-
</Resizable.ResizablePanel>
50-
<Resizable.ResizableHandle />
51-
<Resizable.ResizablePanel
52-
:default-size="defaultTwoPanelLayout[1]"
53-
:min-size="30"
54-
>
55-
<Editor />
56-
</Resizable.ResizablePanel>
57-
</Resizable.ResizablePanelGroup>
58-
<Resizable.ResizablePanelGroup
59-
v-else
60-
direction="horizontal"
61-
class="h-screen"
62-
@layout="onLayout"
33+
<LayoutThreeColumn
34+
:show-sidebar="panels.showSidebar"
35+
:show-list="panels.showList"
36+
:sidebar-width="sidebarWidth"
37+
:list-width="listWidth"
38+
@resize-end="onResizeEnd"
39+
@two-panel-resize="onTwoPanelResize"
6340
>
64-
<Resizable.ResizablePanel
65-
:default-size="defaultThreePanelLayout[0]"
66-
:min-size="10"
67-
>
41+
<template #sidebar>
6842
<Sidebar />
69-
</Resizable.ResizablePanel>
70-
<Resizable.ResizableHandle />
71-
<Resizable.ResizablePanel
72-
:default-size="defaultThreePanelLayout[1]"
73-
:min-size="10"
74-
>
43+
</template>
44+
<template #list>
7545
<SnippetList />
76-
</Resizable.ResizablePanel>
77-
<Resizable.ResizableHandle />
78-
<Resizable.ResizablePanel
79-
:default-size="defaultThreePanelLayout[2]"
80-
:min-size="30"
81-
>
46+
</template>
47+
<template #editor>
8248
<Editor />
83-
</Resizable.ResizablePanel>
84-
</Resizable.ResizablePanelGroup>
49+
</template>
50+
</LayoutThreeColumn>
8551
</template>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<script setup lang="ts">
2+
import { useResizeHandle } from '@/composables'
3+
import { LAYOUT_DEFAULTS } from '~/main/store/constants'
4+
5+
const props = withDefaults(
6+
defineProps<{
7+
showSidebar: boolean
8+
showList: boolean
9+
sidebarWidth?: number
10+
listWidth?: number
11+
}>(),
12+
{
13+
sidebarWidth: LAYOUT_DEFAULTS.sidebar.width,
14+
listWidth: LAYOUT_DEFAULTS.list.width,
15+
},
16+
)
17+
18+
const emit = defineEmits<{
19+
resizeEnd: [sidebarWidth: number, listWidth: number]
20+
twoPanelResize: [listWidth: number]
21+
}>()
22+
23+
const containerRef = ref<HTMLElement>()
24+
const sidebarHandleRef = ref<HTMLElement>()
25+
const listHandleRef = ref<HTMLElement>()
26+
27+
const internalSidebarWidth = ref(props.sidebarWidth)
28+
const internalListWidth = ref(props.listWidth)
29+
30+
function clampWidth(value: number, min: number, max: number) {
31+
return Math.min(max, Math.max(min, value))
32+
}
33+
34+
function getMaxWidth(excludeWidth: number) {
35+
const total = containerRef.value?.clientWidth || window.innerWidth
36+
return total - excludeWidth - LAYOUT_DEFAULTS.editor.min
37+
}
38+
39+
const { isResizing: isSidebarResizing } = useResizeHandle(sidebarHandleRef, {
40+
direction: 'horizontal',
41+
onMove(dx) {
42+
const max = getMaxWidth(internalListWidth.value)
43+
internalSidebarWidth.value = clampWidth(
44+
internalSidebarWidth.value + dx,
45+
LAYOUT_DEFAULTS.sidebar.min,
46+
max,
47+
)
48+
},
49+
onEnd() {
50+
emit('resizeEnd', internalSidebarWidth.value, internalListWidth.value)
51+
},
52+
})
53+
54+
const { isResizing: isListResizing } = useResizeHandle(listHandleRef, {
55+
direction: 'horizontal',
56+
onMove(dx) {
57+
const exclude = props.showSidebar ? internalSidebarWidth.value : 0
58+
const max = getMaxWidth(exclude)
59+
internalListWidth.value = clampWidth(
60+
internalListWidth.value + dx,
61+
LAYOUT_DEFAULTS.list.min,
62+
max,
63+
)
64+
},
65+
onEnd() {
66+
if (props.showSidebar) {
67+
emit('resizeEnd', internalSidebarWidth.value, internalListWidth.value)
68+
}
69+
else {
70+
emit('twoPanelResize', internalListWidth.value)
71+
}
72+
},
73+
})
74+
75+
const isResizing = computed(
76+
() => isSidebarResizing.value || isListResizing.value,
77+
)
78+
</script>
79+
80+
<template>
81+
<div
82+
v-if="!showList"
83+
class="h-screen"
84+
>
85+
<slot name="editor" />
86+
</div>
87+
<div
88+
v-else
89+
ref="containerRef"
90+
class="flex h-screen"
91+
>
92+
<div
93+
v-if="showSidebar"
94+
:style="{ width: `${internalSidebarWidth}px` }"
95+
class="shrink-0 overflow-hidden"
96+
>
97+
<slot name="sidebar" />
98+
</div>
99+
<div
100+
v-if="showSidebar"
101+
ref="sidebarHandleRef"
102+
class="before:bg-border hover:before:bg-primary data-[resizing]:before:bg-primary relative z-10 flex w-px shrink-0 cursor-col-resize items-center justify-center bg-transparent before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2 before:transition-[background-color,width] before:duration-150 before:content-[''] after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2 after:content-[''] hover:before:w-0.5 hover:before:delay-200 data-[resizing]:before:w-0.5"
103+
/>
104+
<div
105+
:style="{ width: `${internalListWidth}px` }"
106+
class="shrink-0 overflow-hidden"
107+
>
108+
<slot name="list" />
109+
</div>
110+
<div
111+
ref="listHandleRef"
112+
class="before:bg-border hover:before:bg-primary data-[resizing]:before:bg-primary relative z-10 flex w-px shrink-0 cursor-col-resize items-center justify-center bg-transparent before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2 before:transition-[background-color,width] before:duration-150 before:content-[''] after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2 after:content-[''] hover:before:w-0.5 hover:before:delay-200 data-[resizing]:before:w-0.5"
113+
/>
114+
<div class="min-w-0 flex-1 overflow-hidden">
115+
<slot name="editor" />
116+
</div>
117+
<div
118+
v-if="isResizing"
119+
class="fixed inset-0 z-50 cursor-col-resize"
120+
/>
121+
</div>
122+
</template>

0 commit comments

Comments
 (0)