Skip to content

Commit aa105f7

Browse files
committed
fix(ci): include mac zip in release artifacts for auto-update
1 parent bf6121d commit aa105f7

File tree

5 files changed

+627
-35
lines changed

5 files changed

+627
-35
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ jobs:
8484
name: macos-build
8585
path: |
8686
release/*.dmg
87+
release/*.zip
8788
release/latest-mac.yml
8889
if-no-files-found: error
8990

src/components/ActivityBar.tsx

Lines changed: 184 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
PanelLeft,
1818
Building2,
1919
Globe,
20-
FileWarning
20+
FileWarning,
21+
ChevronRight
2122
} from 'lucide-react'
2223
import { createContext, useContext, useEffect, useState, useMemo, useRef, useCallback } from 'react'
2324
import { usePDMStore, SidebarView } from '../stores/pdmStore'
@@ -28,7 +29,9 @@ import { logNavigation, logSettings } from '../lib/userActionLogger'
2829
import {
2930
MODULES,
3031
isModuleVisible,
31-
type ModuleId
32+
getChildModules,
33+
type ModuleId,
34+
type ModuleDefinition
3235
} from '../types/modules'
3336

3437
// Custom Google Drive icon
@@ -73,31 +76,69 @@ interface ActivityItemProps {
7376
view: SidebarView
7477
title: string
7578
badge?: number
79+
hasChildren?: boolean
80+
children?: ModuleDefinition[]
81+
depth?: number
82+
onHoverWithChildren?: (moduleId: ModuleId | null, rect: DOMRect | null) => void
7683
}
7784

78-
function ActivityItem({ icon, view, title, badge }: ActivityItemProps) {
85+
function ActivityItem({ icon, view, title, badge, hasChildren, children, depth = 0, onHoverWithChildren }: ActivityItemProps) {
7986
const { activeView, setActiveView } = usePDMStore()
8087
const isExpanded = useContext(ExpandedContext)
8188
const [showTooltip, setShowTooltip] = useState(false)
89+
const [showSubmenu, setShowSubmenu] = useState(false)
90+
const [submenuRect, setSubmenuRect] = useState<DOMRect | null>(null)
91+
const buttonRef = useRef<HTMLButtonElement>(null)
8292
const isActive = activeView === view
93+
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null)
94+
95+
const handleMouseEnter = () => {
96+
if (!isExpanded) setShowTooltip(true)
97+
98+
if (hasChildren && children && children.length > 0) {
99+
// Delay showing submenu slightly to prevent accidental triggers
100+
hoverTimeoutRef.current = setTimeout(() => {
101+
setShowSubmenu(true)
102+
if (buttonRef.current) {
103+
setSubmenuRect(buttonRef.current.getBoundingClientRect())
104+
}
105+
onHoverWithChildren?.(view as ModuleId, buttonRef.current?.getBoundingClientRect() || null)
106+
}, 150)
107+
}
108+
}
109+
110+
const handleMouseLeave = () => {
111+
setShowTooltip(false)
112+
if (hoverTimeoutRef.current) {
113+
clearTimeout(hoverTimeoutRef.current)
114+
hoverTimeoutRef.current = null
115+
}
116+
// Don't close submenu immediately - let the submenu handle its own hover
117+
setTimeout(() => {
118+
setShowSubmenu(false)
119+
onHoverWithChildren?.(null, null)
120+
}, 100)
121+
}
83122

84123
return (
85124
<div className="py-1 px-[6px]">
86125
<button
126+
ref={buttonRef}
87127
onClick={() => {
128+
// If has children, clicking still navigates to the main view
88129
logNavigation(view, { title })
89130
setActiveView(view)
90131
}}
91-
onMouseEnter={() => !isExpanded && setShowTooltip(true)}
92-
onMouseLeave={() => setShowTooltip(false)}
132+
onMouseEnter={handleMouseEnter}
133+
onMouseLeave={handleMouseLeave}
93134
className={`relative h-11 w-full flex items-center gap-3 px-[9px] rounded-lg transition-colors overflow-hidden ${
94135
isActive
95136
? 'text-plm-accent bg-plm-highlight'
96137
: 'text-plm-fg-dim hover:text-plm-fg hover:bg-plm-highlight'
97138
}`}
98139
>
99140
{/* Tooltip for collapsed state */}
100-
{showTooltip && !isExpanded && (
141+
{showTooltip && !isExpanded && !hasChildren && (
101142
<div className="absolute left-full ml-3 z-50 pointer-events-none">
102143
<div className="px-2.5 py-1.5 bg-plm-fg text-plm-bg text-sm font-medium rounded whitespace-nowrap">
103144
{title}
@@ -115,11 +156,130 @@ function ActivityItem({ icon, view, title, badge }: ActivityItemProps) {
115156
)}
116157
</div>
117158
{isExpanded && (
118-
<span className="text-[15px] font-medium whitespace-nowrap overflow-hidden">
119-
{title}
120-
</span>
159+
<>
160+
<span className="text-[15px] font-medium whitespace-nowrap overflow-hidden flex-1">
161+
{title}
162+
</span>
163+
{/* Chevron for items with children */}
164+
{hasChildren && children && children.length > 0 && (
165+
<ChevronRight
166+
size={14}
167+
className={`flex-shrink-0 text-plm-fg-dim transition-transform duration-200 ${showSubmenu ? 'translate-x-0.5' : ''}`}
168+
/>
169+
)}
170+
</>
121171
)}
122172
</button>
173+
174+
{/* Nested Submenu Panel */}
175+
{showSubmenu && hasChildren && children && children.length > 0 && submenuRect && (
176+
<NestedSubmenu
177+
parentRect={submenuRect}
178+
children={children}
179+
depth={depth + 1}
180+
onMouseEnter={() => setShowSubmenu(true)}
181+
onMouseLeave={() => setShowSubmenu(false)}
182+
/>
183+
)}
184+
</div>
185+
)
186+
}
187+
188+
// Nested submenu component that appears on hover
189+
interface NestedSubmenuProps {
190+
parentRect: DOMRect
191+
children: ModuleDefinition[]
192+
depth: number
193+
onMouseEnter: () => void
194+
onMouseLeave: () => void
195+
}
196+
197+
function NestedSubmenu({ parentRect, children, depth, onMouseEnter, onMouseLeave }: NestedSubmenuProps) {
198+
const { activeView, setActiveView, moduleConfig } = usePDMStore()
199+
const { t } = useTranslation()
200+
const [hoveredChild, setHoveredChild] = useState<ModuleId | null>(null)
201+
const [childRect, setChildRect] = useState<DOMRect | null>(null)
202+
const panelRef = useRef<HTMLDivElement>(null)
203+
204+
// Filter to only show visible children
205+
const visibleChildren = children.filter(child => isModuleVisible(child.id, moduleConfig))
206+
207+
if (visibleChildren.length === 0) return null
208+
209+
// Calculate position - always to the right of parent
210+
const style: React.CSSProperties = {
211+
position: 'fixed',
212+
top: parentRect.top - 4,
213+
left: parentRect.right + 4,
214+
zIndex: 50 + depth,
215+
}
216+
217+
return (
218+
<div
219+
ref={panelRef}
220+
style={style}
221+
className="min-w-[180px] bg-plm-activitybar border border-plm-border rounded-lg shadow-xl py-1 animate-in fade-in slide-in-from-left-2 duration-150"
222+
onMouseEnter={onMouseEnter}
223+
onMouseLeave={onMouseLeave}
224+
>
225+
{visibleChildren.map(child => {
226+
const childChildren = getChildModules(child.id, moduleConfig).filter(c => isModuleVisible(c.id, moduleConfig))
227+
const hasGrandchildren = childChildren.length > 0
228+
const translationKey = moduleTranslationKeys[child.id]
229+
const childTitle = translationKey ? t(translationKey) : child.name
230+
const isActive = activeView === child.id
231+
232+
return (
233+
<div
234+
key={child.id}
235+
className="relative"
236+
onMouseEnter={(e) => {
237+
if (hasGrandchildren) {
238+
setHoveredChild(child.id)
239+
const target = e.currentTarget
240+
setChildRect(target.getBoundingClientRect())
241+
}
242+
}}
243+
onMouseLeave={() => {
244+
setTimeout(() => setHoveredChild(null), 100)
245+
}}
246+
>
247+
<button
248+
onClick={() => {
249+
logNavigation(child.id, { title: childTitle })
250+
setActiveView(child.id as SidebarView)
251+
}}
252+
className={`w-full h-10 flex items-center gap-3 px-3 rounded-md mx-1 transition-colors ${
253+
isActive
254+
? 'text-plm-accent bg-plm-highlight'
255+
: 'text-plm-fg-dim hover:text-plm-fg hover:bg-plm-highlight'
256+
}`}
257+
style={{ width: 'calc(100% - 8px)' }}
258+
>
259+
<div className="w-[18px] h-[18px] flex items-center justify-center flex-shrink-0">
260+
{getModuleIcon(child.icon, 18)}
261+
</div>
262+
<span className="text-sm font-medium whitespace-nowrap overflow-hidden flex-1 text-left">
263+
{childTitle}
264+
</span>
265+
{hasGrandchildren && (
266+
<ChevronRight size={12} className="flex-shrink-0 text-plm-fg-dim" />
267+
)}
268+
</button>
269+
270+
{/* Nested children (recursive) */}
271+
{hoveredChild === child.id && hasGrandchildren && childRect && (
272+
<NestedSubmenu
273+
parentRect={childRect}
274+
children={childChildren}
275+
depth={depth + 1}
276+
onMouseEnter={() => setHoveredChild(child.id)}
277+
onMouseLeave={() => setHoveredChild(null)}
278+
/>
279+
)}
280+
</div>
281+
)
282+
})}
123283
</div>
124284
)
125285
}
@@ -287,10 +447,14 @@ export function ActivityBar() {
287447
const totalBadge = unreadNotificationCount + pendingReviewCount
288448

289449
// Build the visible modules list based on module order and visibility
450+
// Only show top-level modules (those without a parent)
290451
const visibleModules = useMemo(() => {
291-
return moduleConfig.moduleOrder.filter(moduleId =>
292-
isModuleVisible(moduleId, moduleConfig)
293-
)
452+
return moduleConfig.moduleOrder.filter(moduleId => {
453+
const module = MODULES.find(m => m.id === moduleId)
454+
// Only show if visible AND is top-level (no parent in config)
455+
const hasParent = moduleConfig.moduleParents?.[moduleId]
456+
return module && !hasParent && isModuleVisible(moduleId, moduleConfig)
457+
})
294458
}, [moduleConfig])
295459

296460
// Build a map of original index to visible index for divider positioning
@@ -403,13 +567,21 @@ export function ActivityBar() {
403567
// Special handling for reviews badge
404568
const badge = moduleId === 'reviews' ? totalBadge : undefined
405569

570+
// Get visible child modules (using config's moduleParents)
571+
const childModules = getChildModules(moduleId, moduleConfig).filter(child =>
572+
isModuleVisible(child.id, moduleConfig)
573+
)
574+
const moduleHasChildren = childModules.length > 0
575+
406576
return (
407577
<div key={moduleId}>
408578
<ActivityItem
409579
icon={getModuleIcon(module.icon)}
410580
view={moduleId as SidebarView}
411581
title={title}
412582
badge={badge}
583+
hasChildren={moduleHasChildren}
584+
children={childModules}
413585
/>
414586
{getDividerAfterVisibleIndex.has(visibleIndex) && <SectionDivider />}
415587
</div>

0 commit comments

Comments
 (0)