Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
66 changes: 33 additions & 33 deletions desktop/frontend/src/components/ProjectTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// new topic.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, DragEvent as ReactDragEvent, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from "react";
import { Archive, ArrowDown, ChevronRight, Pencil, Plus, Folder, FolderPlus, Search, BriefcaseBusiness, Copy, FolderOpen, XCircle, History, Check, ListCollapse, ListRestart, MessageSquare, Clock, Pin, MoreHorizontal, SquarePen, Minimize2, Maximize2 } from "lucide-react";
import { Archive, ArrowDown, ChevronRight, Pencil, Plus, Folder, FolderPlus, Search, BriefcaseBusiness, Copy, FolderOpen, XCircle, History, Check, ListCollapse, ListRestart, MessageSquare, Clock, Pin, MoreHorizontal, Minimize2, Maximize2 } from "lucide-react";
import { asArray } from "../lib/array";
import { app } from "../lib/bridge";
import type { ProjectNode, ProjectTopicStatus } from "../lib/types";
Expand Down Expand Up @@ -1046,8 +1046,8 @@ export function ProjectTree({
</span>
)}
</span>
{compactTopics && (timeLabel || showStatusInSide) && (
<span className="project-tree__topic-side" aria-hidden="true">
{compactTopics && (
<span className={`project-tree__topic-side${!timeLabel && !showStatusInSide ? " project-tree__topic-side--empty" : ""}`} aria-hidden="true">
{showStatusInSide && <span className={`project-tree__topic-state project-tree__topic-state--${status}`} title={statusLabel} />}
{timeLabel && <span className="project-tree__topic-time">{timeLabel}</span>}
</span>
Expand Down Expand Up @@ -1325,7 +1325,7 @@ export function ProjectTree({

if (editingProject?.key === key) {
return (
<div key={key}>
<div key={key} className="project-tree__project-wrapper">
<div
className={`project-tree__folder project-tree__folder--editing${projectActive ? " project-tree__folder--active" : ""}`}
style={{ paddingLeft: 8 + depth * 16 }}
Expand Down Expand Up @@ -1354,7 +1354,7 @@ export function ProjectTree({
}

return (
<div key={key}>
<div key={key} className="project-tree__project-wrapper">
<div
className={`project-tree__folder${scopeClass}${pinnedClass}${draggableProject ? " project-tree__folder--draggable" : ""}${projectActive ? " project-tree__folder--active" : ""}${projectMenuOpen ? " project-tree__folder--menu-open" : ""}${dragProjectRoot === projectDragKey ? " project-tree__folder--dragging" : ""}${projectDropPosition ? ` project-tree__folder--drop-${projectDropPosition}` : ""}`}
style={accentStyle}
Expand All @@ -1374,25 +1374,23 @@ export function ProjectTree({
className="project-tree__folder-main"
style={{ paddingLeft: 8 + depth * 16 }}
onClick={() => {
if (hasChildren) toggleExpand(key);
toggleExpand(key);
}}
onKeyDown={(event) => {
if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) {
openProjectMenu(event);
}
}}
aria-expanded={hasChildren ? isExpanded : undefined}
aria-expanded={isExpanded || undefined}
>
{hasChildren ? (
<span className={`project-tree__chevron${isExpanded ? " project-tree__chevron--open" : ""}`}>
<ChevronRight size={12} />
<span className="project-tree__icon-stack">
<span className={`project-tree__chevron project-tree__chevron--on-hover${isExpanded ? " project-tree__chevron--open" : ""}`}>
<ChevronRight size={16} strokeWidth={2} />
</span>
) : (
<span style={{ width: 12 }} />
)}
<Folder size={12} />
{isExpanded ? <FolderOpen size={14} className="project-tree__folder-icon" /> : <Folder size={14} className="project-tree__folder-icon" />}
</span>
<span className="project-tree__folder-color" aria-hidden="true" />
<span className="project-tree__folder-label">{projectLabel}</span>
<span className={`project-tree__folder-label${!hasChildren ? " project-tree__folder-label--empty" : ""}`}>{projectLabel}</span>
</button>
{compactTopics && (
<Tooltip label={t("projectTree.projectActions")} className="project-tree__folder-action-slot">
Expand Down Expand Up @@ -1423,7 +1421,7 @@ export function ProjectTree({
void handleCreateTopic(scope, projectRoot, key);
}}
>
{compactTopics ? <SquarePen size={15} aria-hidden="true" /> : <Plus size={12} aria-hidden="true" />}
{compactTopics ? <Plus size={15} aria-hidden="true" /> : <Plus size={12} aria-hidden="true" />}
</button>
</Tooltip>
<ContextMenu
Expand Down Expand Up @@ -1801,24 +1799,26 @@ export function ProjectTree({
/>
</label>
{compactTopics ? (
<div className="project-tree__list project-tree__list--workbench">
{!hasWorkbenchRows ? (
renderEmptyState()
) : (
<>
{workbenchTreeSections.pinned.length > 0 && (
<div className="project-tree__section project-tree__section--pinned">
<div className="project-tree__section-title">{t("projectTree.pinnedTitle")}</div>
{workbenchTreeSections.pinned.map((node) => renderNode(node, 0, "pinned"))}
<>
{renderProjectHeader("workbench")}
<div className="project-tree__list project-tree__list--workbench">
{!hasWorkbenchRows ? (
renderEmptyState()
) : (
<>
{workbenchTreeSections.pinned.length > 0 && (
<div className="project-tree__section project-tree__section--pinned">
<div className="project-tree__section-title">{t("projectTree.pinnedTitle")}</div>
{workbenchTreeSections.pinned.map((node) => renderNode(node, 0, "pinned"))}
</div>
)}
<div className="project-tree__section project-tree__section--projects">
{workbenchTreeSections.projects.map((node) => renderNode(node, 0, "projects"))}
</div>
)}
<div className="project-tree__section project-tree__section--projects">
{renderProjectHeader("workbench")}
{workbenchTreeSections.projects.map((node) => renderNode(node, 0, "projects"))}
</div>
</>
)}
</div>
</>
)}
</div>
</>
) : (
<>
{renderProjectHeader("classic")}
Expand Down
170 changes: 169 additions & 1 deletion desktop/frontend/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -17110,7 +17110,7 @@ a[href] {
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
transition: grid-template-rows 0.2s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.16s ease, transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
transition: opacity 0.16s ease, transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.project-tree__children--expanded {
Expand Down Expand Up @@ -17139,6 +17139,174 @@ a[href] {
transform: rotate(90deg);
}

/* Project tree: icon stack holds chevron + folder icon at same position */
.project-tree__icon-stack {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex: 0 0 auto;
position: relative;
}
.project-tree__icon-stack > .project-tree__folder-icon {
width: 14px;
height: 14px;
color: var(--fg-faint);
}
.project-tree__icon-stack > .project-tree__chevron {
position: absolute;
color: var(--fg-faint);
}
/* Override workbench width constraint on chevron */
.sidebar--workbench .project-tree__icon-stack > .project-tree__chevron {
width: auto;
height: auto;
}
.project-tree__icon-stack > .project-tree__chevron > svg {
display: block;
}
/* Default: hide chevron, show folder icon */
.project-tree__chevron--on-hover {
display: none;
}
/* Hover: show chevron, hide folder icon */
:root[data-theme-style] .project-tree__folder-main:hover .project-tree__chevron--on-hover,
.project-tree__folder-main:hover .project-tree__chevron--on-hover {
display: inline-flex;
}
:root[data-theme-style] .project-tree__folder-main:hover .project-tree__icon-stack > .project-tree__folder-icon,
.project-tree__folder-main:hover .project-tree__icon-stack > .project-tree__folder-icon {
display: none;
}
/* Topic hover/active effect: full row width (remove left margin) */
:root[data-theme-style] .sidebar--workbench .project-tree__topic {
margin-left: 0;
}

/* Project name de-emphasis: empty projects (no children) use lighter color */
:root[data-theme-style] .project-tree__folder-label--empty {
color: var(--fg-faint);
}
:root[data-theme-style] .project-tree__folder-label:not(.project-tree__folder-label--empty) {
color: color-mix(in srgb, var(--fg-dim) 58%, var(--fg-faint) 42%);
}
:root[data-theme-style] .project-tree__topic-label {
color: var(--fg);
font-weight: 550;
}

/* Extra spacing after expanded project section */
/* Extra spacing only after expanded project that actually has children */
.project-tree__project-wrapper {
contain: layout;
}
.project-tree__project-wrapper:has(.project-tree__folder-main[aria-expanded="true"]):has(.project-tree__children) {
margin-bottom: 6px;
}
.project-tree__project-wrapper:has(.project-tree__folder-main[aria-expanded="true"]):has(.project-tree__children) + .project-tree__project-wrapper {
margin-top: 6px;
}

/* Weaken topic time display */
:root[data-theme-style] .sidebar--workbench .project-tree__topic-side {
font-size: 11px;
color: color-mix(in srgb, var(--fg-faint) 72%, transparent);
}
:root[data-theme-style] .sidebar--workbench .project-tree__topic-side--empty {
visibility: hidden;
}
/* Ensure consistent topic height regardless of time/status presence */
:root[data-theme-style] .sidebar--workbench .project-tree__topic {
min-height: 34px;
height: 34px;
box-sizing: border-box;
}
:root[data-theme-style] .sidebar--workbench .project-tree__topic-main {
min-height: 34px;
height: 34px;
box-sizing: border-box;
}

/* Sidebar: keep search & header fixed, only list scrolls */
:root[data-theme-style] .sidebar--workbench .project-tree {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
:root[data-theme-style] .sidebar--workbench .project-tree__search,
:root[data-theme-style] .sidebar--workbench .project-tree__header {
flex: 0 0 auto;
}
:root[data-theme-style] .sidebar--workbench .project-tree__list,
:root[data-theme-style] .sidebar--workbench .project-tree__list--workbench {
flex: 1;
min-height: 0;
overflow-y: auto;
}

/* Folder: no active background, but very weak hover hint */
:root[data-theme-style] .sidebar--workbench .project-tree__folder:hover {
background: color-mix(in srgb, var(--sidebar-hover) 60%, transparent);
}
:root[data-theme-style] .sidebar--workbench .project-tree__folder--active,
:root[data-theme-style] .sidebar--workbench .project-tree__folder--menu-open,
:root[data-theme-style] .sidebar--workbench .project-tree__folder--active:hover,
:root[data-theme-style] .sidebar--workbench .project-tree__folder--menu-open:hover {
background: transparent;
}

/* Topic name left-aligned with project name */
:root[data-theme-style] .sidebar--workbench .project-tree__topic-main {
padding-left: 30px !important;
}

/* Active topic: keep text style unchanged (no color/font-weight change) */
:root[data-theme-style] .sidebar--workbench .project-tree__topic--active .project-tree__topic-label {
color: var(--fg);
font-weight: 550;
}

/* Topic action icons (pin, archive): clean, no background, no border */
:root[data-theme-style] .sidebar--workbench .project-tree__topic-action {
background: transparent;
border-color: transparent;
box-shadow: none;
}
:root[data-theme-style] .sidebar--workbench .project-tree__topic-action:hover {
background: transparent;
border-color: transparent;
box-shadow: none;
transform: none;
}

/* Folder action icons: hidden by default, show on folder hover */
:root[data-theme-style] .sidebar--workbench .project-tree__folder-action-slot {
opacity: 0;
pointer-events: none;
transition: opacity var(--dur-fast);
}
:root[data-theme-style] .sidebar--workbench .project-tree__folder:hover .project-tree__folder-action-slot {
opacity: 1;
pointer-events: auto;
}

/* Remove right-side slide animations on topic hover */
:root[data-theme-style] .sidebar--workbench .project-tree__topic-actions {
transform: translateY(-50%) translateX(0);
}
:root[data-theme-style] .sidebar--workbench .project-tree__topic:hover .project-tree__topic-actions,
:root[data-theme-style] .sidebar--workbench .project-tree__topic:focus-within .project-tree__topic-actions,
:root[data-theme-style] .sidebar--workbench .project-tree__topic--menu-open .project-tree__topic-actions {
transform: translateY(-50%) translateX(0);
}
:root[data-theme-style] .sidebar--workbench .project-tree__topic:hover .project-tree__topic-side,
:root[data-theme-style] .sidebar--workbench .project-tree__topic:focus-within .project-tree__topic-side,
:root[data-theme-style] .sidebar--workbench .project-tree__topic--menu-open .project-tree__topic-side {
transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
.project-tree__children,
.project-tree__chevron {
Expand Down
Loading