1+ import { useState } from "react"
12import { cn } from "@/lib/utils"
23import { useAppStore , type SidebarView } from "@/stores/app-store"
34import { Button } from "@/components/ui/button"
5+ import { X , ArrowUpRight } from "lucide-react"
6+ import { useUpdateCheck } from "@/hooks/use-update-check"
7+ import { UpdateDialog } from "./update-dialog"
48
5- /* Small diamond bullet — inline style overrides Tailwind SVG size rule */
9+ /* Small diamond bullet */
610const DiamondDot = ( ) => (
711 < svg style = { { width : "7px" , height : "7px" } } viewBox = "0 0 4 4" fill = "currentColor" stroke = "none" >
812 < polygon points = "2,0 4,2 2,4 0,2" />
@@ -19,47 +23,158 @@ const navItems: Array<{ view: SidebarView; label: string }> = [
1923
2024export function Sidebar ( ) {
2125 const { sidebarView, setSidebarView } = useAppStore ( )
26+ const { update, ignored, ignoreVersion, currentVersion } = useUpdateCheck ( )
27+ const [ dialogOpen , setDialogOpen ] = useState ( false )
28+ const [ leaving , setLeaving ] = useState ( false )
29+
30+ const showCard = update && ! ignored && ! leaving
31+ const showDot = update !== null
32+
33+ const handleIgnore = ( ) => {
34+ setLeaving ( true )
35+ setTimeout ( ( ) => {
36+ ignoreVersion ( )
37+ setLeaving ( false )
38+ } , 250 )
39+ }
2240
2341 return (
24- < aside
25- className = "w-[172px] border-r border-border flex flex-col shrink-0 py-6 px-4 bg-background"
26- >
27- < nav className = "flex flex-col flex-1" >
28- < div className = "text-[14px] font-[300] uppercase tracking-[0.25em] text-muted-foreground mb-4" >
29- Navigate
30- </ div >
42+ < >
43+ < aside
44+ className = "w-[172px] border-r border-border flex flex-col shrink-0 py-6 px-4 bg-background"
45+ >
46+ < nav className = "flex flex-col flex-1" >
47+ < div className = "text-[14px] font-[300] uppercase tracking-[0.25em] text-muted-foreground mb-4" >
48+ Navigate
49+ </ div >
3150
32- { navItems . map ( ( { view, label } ) => (
33- < div key = { view } className = "mb-0.5" >
34- < Button
35- variant = "ghost"
36- className = { cn (
37- "w-full justify-start gap-2.5 py-2 px-0 h-auto text-xs uppercase tracking-wider relative rounded-none" ,
38- "hover:bg-transparent hover:text-primary" ,
39- sidebarView === view ? "font-[400] text-primary" : "font-[300] text-muted-foreground" ,
40- ) }
41- onClick = { ( ) => setSidebarView ( view ) }
51+ { navItems . map ( ( { view, label } ) => (
52+ < div key = { view } className = "mb-0.5" >
53+ < Button
54+ variant = "ghost"
55+ className = { cn (
56+ "w-full justify-start gap-2.5 py-2 px-0 h-auto text-xs uppercase tracking-wider relative rounded-none" ,
57+ "hover:bg-transparent hover:text-primary" ,
58+ sidebarView === view ? "font-[400] text-primary" : "font-[300] text-muted-foreground" ,
59+ ) }
60+ onClick = { ( ) => setSidebarView ( view ) }
61+ >
62+ < span style = { { display : "inline-flex" , alignItems : "center" , justifyContent : "center" , width : "8px" , height : "8px" , flexShrink : 0 , opacity : sidebarView === view ? 1 : 0.4 , lineHeight : 0 } } >
63+ < DiamondDot />
64+ </ span >
65+ { label }
66+ { sidebarView === view && (
67+ < span
68+ className = "absolute bottom-0 left-0 h-[1.5px] w-5"
69+ style = { { background : "var(--ze-green)" } }
70+ />
71+ ) }
72+ </ Button >
73+ </ div >
74+ ) ) }
75+
76+ { /* ── Bottom group: update card + version line ── */ }
77+ < div className = "mt-auto" >
78+ < div
79+ className = { cn (
80+ "overflow-hidden transition-all duration-300 ease-out" ,
81+ showCard ? "translate-x-0 opacity-100 max-h-32 mb-3" : "-translate-x-full opacity-0 max-h-0 mb-0"
82+ ) }
83+ >
84+ { update && ! ignored && (
85+ < div
86+ className = "rounded-sm p-3"
87+ style = { {
88+ border : "1px solid #1a3a2a" ,
89+ backgroundColor : "#faf7f2" ,
90+ boxShadow : "0 1px 3px rgba(0,0,0,0.04)" ,
91+ } }
4292 >
43- < span style = { { display : "inline-flex" , alignItems : "center" , justifyContent : "center" , width : "8px" , height : "8px" , flexShrink : 0 , opacity : sidebarView === view ? 1 : 0.4 , lineHeight : 0 } } >
44- < DiamondDot />
93+ { /* Header row */ }
94+ < div className = "flex items-center justify-between mb-2" >
95+ < div className = "flex items-center gap-1.5" >
96+ < span style = { { color : "#1a3a2a" , lineHeight : 0 } } >
97+ < DiamondDot />
98+ </ span >
99+ < span
100+ className = "text-[10px] uppercase tracking-[0.12em]"
101+ style = { { color : "#1a3a2a" , fontFamily : "var(--font-serif)" } }
102+ >
103+ New Version
104+ </ span >
105+ </ div >
106+ < button
107+ onClick = { handleIgnore }
108+ className = "text-muted-foreground/50 hover:text-muted-foreground transition-colors"
109+ title = "Ignore this version"
110+ >
111+ < X className = "h-3 w-3" />
112+ </ button >
113+ </ div >
114+
115+ { /* Version number */ }
116+ < p
117+ className = "text-sm font-light mb-3"
118+ style = { { fontFamily : "var(--font-serif)" , color : "var(--ze-ink)" } }
119+ >
120+ { update . latestVersion }
121+ </ p >
122+
123+ { /* Buttons */ }
124+ < div className = "flex items-center gap-2" >
125+ < button
126+ onClick = { handleIgnore }
127+ className = "text-[10px] uppercase tracking-[0.08em] text-muted-foreground/60 hover:text-muted-foreground transition-colors"
128+ style = { { fontFamily : "var(--font-serif)" } }
129+ >
130+ Ignore
131+ </ button >
132+ < button
133+ onClick = { ( ) => setDialogOpen ( true ) }
134+ className = "text-[10px] uppercase tracking-[0.08em] font-medium transition-colors flex items-center gap-1"
135+ style = { { color : "#1a3a2a" , fontFamily : "var(--font-serif)" } }
136+ >
137+ Update < ArrowUpRight className = "h-2.5 w-2.5" />
138+ </ button >
139+ </ div >
140+ </ div >
141+ ) }
142+ </ div >
143+
144+ { /* ── Version line ── */ }
145+ < div className = "pt-5 border-t border-dashed border-border" >
146+ < div className = "flex items-center justify-between" >
147+ < span
148+ className = "text-[10px] font-[300] tracking-[0.1em] text-muted-foreground/60"
149+ style = { { fontFamily : "var(--font-serif)" } }
150+ >
151+ v{ currentVersion }
45152 </ span >
46- { label }
47- { sidebarView === view && (
48- < span
49- className = "absolute bottom-0 left-0 h-[1.5px] w-5"
50- style = { { background : "var(--ze-green)" } }
51- />
153+ { showDot && (
154+ < button
155+ onClick = { ( ) => setDialogOpen ( true ) }
156+ className = "flex items-center gap-1 text-[10px] font-[300] tracking-[0.08em] hover:opacity-80 transition-opacity"
157+ style = { { color : "#dc2626" , fontFamily : "var(--font-serif)" } }
158+ title = { `Update ${ update . latestVersion } available` }
159+ >
160+ < span className = "w-1 h-1 rounded-full" style = { { backgroundColor : "#dc2626" } } />
161+ { update . latestVersion . replace ( / ^ v / , "" ) }
162+ </ button >
52163 ) }
53- </ Button >
164+ </ div >
54165 </ div >
55- ) ) }
56-
57- < div className = "mt-auto pt-5 border-t border-dashed border-border" >
58- < div className = "text-[10px] font-[300] tracking-[0.1em] text-muted-foreground/60" >
59- SinkDuce v0.1 · RAG
60166 </ div >
61- </ div >
62- </ nav >
63- </ aside >
167+ </ nav >
168+ </ aside >
169+
170+ { /* ── Update detail dialog ── */ }
171+ { update && (
172+ < UpdateDialog
173+ open = { dialogOpen }
174+ onOpenChange = { setDialogOpen }
175+ update = { update }
176+ />
177+ ) }
178+ </ >
64179 )
65180}
0 commit comments