Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions css/trailSidebar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/* Trail Sidebar - Tree view of browsing trails */

.trail-sidebar {
position: fixed;
left: 0;
top: calc(36px + var(--control-space-top, 0px));
width: 280px;
height: calc(100% - 36px - var(--control-space-top, 0px));
background-color: #fff;
border-right: 1px solid rgba(0, 0, 0, 0.1);
overflow-y: auto;
z-index: 100;
display: flex;
flex-direction: column;
transition: transform 0.15s ease-out;
}

.dark-mode .trail-sidebar {
background-color: rgb(33, 37, 43);
border-right-color: rgba(255, 255, 255, 0.1);
}

.trail-sidebar[hidden] {
display: none;
}

/* Adjust webviews when sidebar is visible */
body.trail-sidebar-visible #webviews {
margin-left: 280px;
width: calc(100% - 280px);
}

body.trail-sidebar-visible #searchbar {
margin-left: 280px;
width: calc(100% - 280px);
}

/* Sidebar Header */
.trail-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
flex-shrink: 0;
}

.dark-mode .trail-sidebar-header {
border-bottom-color: rgba(255, 255, 255, 0.08);
}

.trail-sidebar-title {
font-size: 13px;
font-weight: 600;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.trail-sidebar-close {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
opacity: 0.6;
transition: opacity 0.1s, background-color 0.1s;
}

.trail-sidebar-close:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.08);
}

.dark-mode .trail-sidebar-close:hover {
background-color: rgba(255, 255, 255, 0.08);
}

/* Tree Container */
.trail-tree {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}

/* Trail Node */
.trail-node {
padding: 6px 12px;
padding-left: calc(12px + var(--depth, 0) * 16px);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: inherit;
transition: background-color 0.1s;
user-select: none;
}

.trail-node:hover {
background-color: rgba(0, 0, 0, 0.05);
}

.dark-mode .trail-node:hover {
background-color: rgba(255, 255, 255, 0.05);
}

.trail-node.selected {
background-color: rgba(0, 117, 255, 0.12);
}

.dark-mode .trail-node.selected {
background-color: rgba(30, 144, 255, 0.2);
}

.trail-node.selected:hover {
background-color: rgba(0, 117, 255, 0.18);
}

.dark-mode .trail-node.selected:hover {
background-color: rgba(30, 144, 255, 0.25);
}

/* Collapse Button */
.trail-collapse-btn {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
opacity: 0.6;
flex-shrink: 0;
transition: opacity 0.1s, transform 0.15s;
}

.trail-collapse-btn:not(.trail-collapse-btn-leaf):hover {
opacity: 1;
}

.trail-collapse-btn-leaf {
opacity: 0.3;
font-size: 8px;
}

/* Favicon */
.trail-favicon {
width: 16px;
height: 16px;
flex-shrink: 0;
border-radius: 2px;
}

/* Title */
.trail-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.9;
}

.trail-node.selected .trail-title {
opacity: 1;
font-weight: 500;
}

/* Scrollbar styling */
.trail-tree::-webkit-scrollbar {
width: 6px;
}

.trail-tree::-webkit-scrollbar-track {
background-color: transparent;
}

.trail-tree::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 3px;
}

.trail-tree::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.25);
}

.dark-mode .trail-tree::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
}

.dark-mode .trail-tree::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.3);
}

/* Depth visual guides (optional lines) */
.trail-node::before {
content: '';
position: absolute;
left: calc(12px + (var(--depth, 0) - 1) * 16px + 7px);
top: 0;
bottom: 0;
width: 1px;
background-color: rgba(0, 0, 0, 0.08);
display: none;
}

.dark-mode .trail-node::before {
background-color: rgba(255, 255, 255, 0.08);
}

/* Show guide lines for depth > 0 */
.trail-node[style*="--depth: 1"]::before,
.trail-node[style*="--depth: 2"]::before,
.trail-node[style*="--depth: 3"]::before,
.trail-node[style*="--depth: 4"]::before,
.trail-node[style*="--depth: 5"]::before {
display: none; /* Disabled for now - can enable for tree lines */
}

/* Empty state */
.trail-tree:empty::after {
content: 'No tabs open';
display: block;
padding: 20px;
text-align: center;
opacity: 0.5;
font-size: 13px;
}
7 changes: 6 additions & 1 deletion js/browserUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var focusMode = require('focusMode.js')
var tabBar = require('navbar/tabBar.js')
var tabEditor = require('navbar/tabEditor.js')
var searchbar = require('searchbar/searchbar.js')
var trailSidebar = require('trailSidebar/trailSidebar.js')

/* creates a new task */

Expand Down Expand Up @@ -300,6 +301,9 @@ tabBar.events.on('tab-closed', function (id) {
closeTab(id)
})

// Initialize trail sidebar
trailSidebar.initialize()

module.exports = {
addTask,
addTab,
Expand All @@ -310,5 +314,6 @@ module.exports = {
switchToTask,
switchToTab,
moveTabLeft,
moveTabRight
moveTabRight,
toggleTrailSidebar: () => trailSidebar.toggle()
}
9 changes: 9 additions & 0 deletions js/defaultKeybindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ const defaultKeybindings = {
})
}
})

// Toggle trail sidebar (Cmd+\ or Cmd+B)
keybindings.defineShortcut({ keys: 'mod+\\' }, function () {
browserUI.toggleTrailSidebar()
})

keybindings.defineShortcut('toggleTrailSidebar', function () {
browserUI.toggleTrailSidebar()
})
}
}

Expand Down
46 changes: 43 additions & 3 deletions js/tabState/tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@ class TabList {
scrollPosition: tab.scrollPosition || 0,
selected: tab.selected || false,
muted: tab.muted || false,
loaded: tab.loaded || false,
loaded: tab.loaded || false,
hasAudio: false,
previewImage: '',
isFileView: false,
hasWebContents: false,
// Trail tree properties
parentId: tab.parentId || null, // ID of parent tab (null = root)
childIds: tab.childIds || [], // Array of child tab IDs
collapsed: tab.collapsed || false, // Whether children are hidden
trailName: tab.trailName || null, // Optional name for this branch
trailEmoji: tab.trailEmoji || null, // Optional emoji
depth: tab.depth || 0, // Nesting level for indentation
}

if (options.atEnd) {
Expand Down Expand Up @@ -73,6 +80,39 @@ class TabList {
if (index < 0) return false

const containingTask = this.parentTaskList.getTaskContainingTab(id).id
const tab = this.tabs[index]

// Handle children: reparent them to the destroyed tab's parent (grandparent)
const childIds = tab.childIds || []
const parentId = tab.parentId || null

childIds.forEach(childId => {
const childIndex = this.getIndex(childId)
if (childIndex >= 0) {
// Update child's parentId to grandparent
this.tabs[childIndex].parentId = parentId
// Recalculate depth
this.tabs[childIndex].depth = this.parentTaskList.calculateDepth(childId)

if (emit) {
this.parentTaskList.emit('tab-reparented', childId, parentId, containingTask)
}
}
})

// If this tab had a parent, remove this tab from parent's childIds
if (parentId) {
const parentIndex = this.getIndex(parentId)
if (parentIndex >= 0) {
const parentChildIds = this.tabs[parentIndex].childIds || []
const childIdIndex = parentChildIds.indexOf(id)
if (childIdIndex >= 0) {
parentChildIds.splice(childIdIndex, 1)
}
// Add this tab's children to the parent's childIds
this.tabs[parentIndex].childIds = parentChildIds.concat(childIds)
}
}

tasks.getTaskContainingTab(id).tabHistory.push(this.toPermanentState(this.tabs[index]))
this.tabs.splice(index, 1)
Expand Down Expand Up @@ -188,7 +228,7 @@ class TabList {

splice (...args) {
const containingTask = this.parentTaskList.find(t => t.tabs === this).id

this.parentTaskList.emit('tab-splice', containingTask, ...args)
return this.tabs.splice.apply(this.tabs, args)
}
Expand All @@ -204,7 +244,7 @@ class TabList {
Object.keys(tab)
.filter(key => !TabList.temporaryProperties.includes(key))
.forEach(key => result[key] = tab[key])

return result
}

Expand Down
Loading