Skip to content

Commit d156270

Browse files
Copilotbarelyhuman
andauthored
Add multi-tab support with keyboard shortcuts and per-tab exports (#40)
* Initial plan * Initial commit: Planning tab functionality implementation Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * Implement tab functionality with sidebar and keyboard shortcuts Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * Fix tab renaming functionality and verify build Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * Address code review feedback: fix deprecated substr, add validation, improve input focusing Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * Security: Update html2pdf.js from 0.10.1 to 0.14.0 to fix XSS vulnerability Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * Fix sidebar width issue - add flex-shrink: 0 and min-width to prevent collapse Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * Remove unnecessary derivative state in sidebar component Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> * feat: update sidebar styles and add margin to body * fix: update caniuse-lite version in pnpm-lock.yaml --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: barelyhuman <43572006+barelyhuman@users.noreply.github.com> Co-authored-by: Siddharth Gelera <ahoy@barelyhuman.dev>
1 parent 00e7c45 commit d156270

8 files changed

Lines changed: 5285 additions & 103 deletions

File tree

package-lock.json

Lines changed: 4668 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"dom-to-image": "^2.6.0",
2222
"downloadjs": "^1.4.7",
2323
"highlight.js": "^11.11.1",
24-
"html2pdf.js": "0.10.1",
24+
"html2pdf.js": "^0.14.0",
2525
"marked": "^14.1.4",
2626
"md-to-quill-delta": "^1.1.1",
2727
"quill": "^2.0.3",

pnpm-lock.yaml

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/base-layout.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="min-w-screen min-h-screen flex flex-col p-5">
2+
<div class="min-w-screen min-h-screen flex flex-col p-5 flex-1 overflow-auto">
33
<slot />
44
</div>
55
</template>

src/components/sidebar.vue

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<template>
2+
<div class="sidebar">
3+
<div class="sidebar-header">
4+
<h3 class="sidebar-title">Files</h3>
5+
<button
6+
class="new-tab-button"
7+
@click="handleNewTab"
8+
title="New Tab (Cmd/Ctrl + T)"
9+
>
10+
<svg
11+
xmlns="http://www.w3.org/2000/svg"
12+
width="20"
13+
height="20"
14+
viewBox="0 0 24 24"
15+
stroke-width="2"
16+
stroke="currentColor"
17+
fill="none"
18+
stroke-linecap="round"
19+
stroke-linejoin="round"
20+
>
21+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
22+
<line x1="12" y1="5" x2="12" y2="19"></line>
23+
<line x1="5" y1="12" x2="19" y2="12"></line>
24+
</svg>
25+
</button>
26+
</div>
27+
<div class="tabs-list">
28+
<div
29+
v-for="tab in tabs"
30+
:key="tab.id"
31+
class="tab-item"
32+
:class="{ active: tab.id === tabsState.activeTabId }"
33+
@click="handleTabClick(tab.id)"
34+
>
35+
<input
36+
v-if="editingTabId === tab.id"
37+
v-model="editingTitle"
38+
@blur="handleTitleBlur"
39+
@keydown.enter="handleTitleBlur"
40+
@keydown.esc="cancelEdit"
41+
class="tab-title-input"
42+
:data-tab-id="tab.id"
43+
@click.stop
44+
/>
45+
<span v-else class="tab-title" @dblclick="startEdit(tab)">
46+
{{ tab.title }}
47+
</span>
48+
<button
49+
v-if="tabs.length > 1"
50+
class="close-tab-button"
51+
@click.stop="handleCloseTab(tab.id)"
52+
title="Close Tab"
53+
>
54+
<svg
55+
xmlns="http://www.w3.org/2000/svg"
56+
width="16"
57+
height="16"
58+
viewBox="0 0 24 24"
59+
stroke-width="2"
60+
stroke="currentColor"
61+
fill="none"
62+
stroke-linecap="round"
63+
stroke-linejoin="round"
64+
>
65+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
66+
<line x1="18" y1="6" x2="6" y2="18"></line>
67+
<line x1="6" y1="6" x2="18" y2="18"></line>
68+
</svg>
69+
</button>
70+
</div>
71+
</div>
72+
</div>
73+
</template>
74+
75+
<script setup>
76+
import { ref, nextTick } from "vue";
77+
import {
78+
tabsState,
79+
createTab,
80+
setActiveTab,
81+
closeTab,
82+
updateTabTitle,
83+
} from "../stores/tabs.js";
84+
85+
const tabs = tabsState.tabs;
86+
const editingTabId = ref(null);
87+
const editingTitle = ref("");
88+
89+
function handleNewTab() {
90+
createTab();
91+
}
92+
93+
function handleTabClick(tabId) {
94+
setActiveTab(tabId);
95+
}
96+
97+
function handleCloseTab(tabId) {
98+
closeTab(tabId);
99+
}
100+
101+
function startEdit(tab) {
102+
editingTabId.value = tab.id;
103+
editingTitle.value = tab.title;
104+
nextTick(() => {
105+
const input = document.querySelector(`input[data-tab-id="${tab.id}"]`);
106+
if (input) {
107+
input.focus();
108+
input.select();
109+
}
110+
});
111+
}
112+
113+
function handleTitleBlur() {
114+
if (editingTabId.value && editingTitle.value.trim()) {
115+
updateTabTitle(editingTabId.value, editingTitle.value.trim());
116+
}
117+
editingTabId.value = null;
118+
editingTitle.value = "";
119+
}
120+
121+
function cancelEdit() {
122+
editingTabId.value = null;
123+
editingTitle.value = "";
124+
}
125+
</script>
126+
127+
<style scoped>
128+
.sidebar {
129+
width: 250px;
130+
min-width: 250px;
131+
flex-shrink: 0;
132+
background: var(--overlay);
133+
border-right: 1px solid var(--overlay);
134+
display: flex;
135+
flex-direction: column;
136+
height: 100vh;
137+
overflow: hidden;
138+
position: relative;
139+
z-index: 10;
140+
}
141+
142+
.sidebar-header {
143+
display: flex;
144+
justify-content: space-between;
145+
align-items: center;
146+
padding: 1rem;
147+
border-bottom: 1px solid var(--overlay);
148+
}
149+
150+
.sidebar-title {
151+
font-size: 0.875rem;
152+
font-weight: 600;
153+
margin: 0;
154+
color: var(--text);
155+
}
156+
157+
.new-tab-button {
158+
background: none;
159+
border: none;
160+
cursor: pointer;
161+
padding: 0.25rem;
162+
color: var(--text);
163+
opacity: 0.7;
164+
transition: opacity 0.2s;
165+
display: flex;
166+
align-items: center;
167+
justify-content: center;
168+
position: relative;
169+
z-index: 20;
170+
}
171+
172+
.new-tab-button:hover {
173+
opacity: 1;
174+
}
175+
176+
.tabs-list {
177+
flex: 1;
178+
overflow-y: auto;
179+
padding: 0.5rem 0;
180+
}
181+
182+
.tab-item {
183+
display: flex;
184+
align-items: center;
185+
justify-content: space-between;
186+
padding: 0.625rem 1rem;
187+
cursor: pointer;
188+
transition: background-color 0.2s;
189+
gap: 0.5rem;
190+
position: relative;
191+
z-index: 15;
192+
}
193+
194+
.tab-item:hover {
195+
background-color: var(--hover, rgba(0, 0, 0, 0.05));
196+
}
197+
198+
.tab-item.active {
199+
background-color: var(--active, rgba(0, 0, 0, 0.1));
200+
font-weight: 500;
201+
}
202+
203+
.tab-title {
204+
flex: 1;
205+
overflow: hidden;
206+
text-overflow: ellipsis;
207+
white-space: nowrap;
208+
font-size: 0.875rem;
209+
color: var(--text);
210+
}
211+
212+
.tab-title-input {
213+
flex: 1;
214+
background: var(--base);
215+
border: 1px solid var(--border, #e1e4e8);
216+
border-radius: 4px;
217+
padding: 0.25rem 0.5rem;
218+
font-size: 0.875rem;
219+
color: var(--text);
220+
outline: none;
221+
}
222+
223+
.tab-title-input:focus {
224+
border-color: var(--accent, #0066ff);
225+
}
226+
227+
.close-tab-button {
228+
background: none;
229+
border: none;
230+
cursor: pointer;
231+
padding: 0.125rem;
232+
color: var(--text);
233+
opacity: 0;
234+
transition: opacity 0.2s;
235+
display: flex;
236+
align-items: center;
237+
justify-content: center;
238+
}
239+
240+
.tab-item:hover .close-tab-button {
241+
opacity: 0.5;
242+
}
243+
244+
.close-tab-button:hover {
245+
opacity: 1 !important;
246+
}
247+
248+
/* Scrollbar styling */
249+
.tabs-list::-webkit-scrollbar {
250+
width: 6px;
251+
}
252+
253+
.tabs-list::-webkit-scrollbar-track {
254+
background: transparent;
255+
}
256+
257+
.tabs-list::-webkit-scrollbar-thumb {
258+
background: var(--border, #e1e4e8);
259+
border-radius: 3px;
260+
}
261+
262+
.tabs-list::-webkit-scrollbar-thumb:hover {
263+
background: var(--text, #555);
264+
}
265+
</style>

0 commit comments

Comments
 (0)