@@ -77,7 +77,7 @@ struct Sidebar: View {
7777 BrandMark ( )
7878 VStack ( alignment: . leading, spacing: 1 ) {
7979 Text ( " BloatMac " ) . font ( . system( size: 14 , weight: . bold) ) . tracking ( - 0.3 ) . foregroundStyle ( Tokens . text)
80- Text ( " v 2.4.1 " ) . font ( . system( size: 10 , weight: . medium) ) . foregroundStyle ( Tokens . text3)
80+ Text ( appVersionLabel ) . font ( . system( size: 10 , weight: . medium) ) . foregroundStyle ( Tokens . text3)
8181 }
8282 }
8383 . padding ( . horizontal, 14 ) . padding ( . bottom, 14 )
@@ -108,15 +108,135 @@ struct Sidebar: View {
108108 }
109109}
110110
111+ /// Reads the bundle's CFBundleShortVersionString so the sidebar always
112+ /// reflects the actual build, not a hardcoded prototype string. Falls
113+ /// back to the build number if the short version key is missing.
114+ var appVersionLabel : String {
115+ let info = Bundle . main. infoDictionary
116+ let short = info ? [ " CFBundleShortVersionString " ] as? String
117+ let build = info ? [ " CFBundleVersion " ] as? String
118+ return " v " + ( short ?? build ?? " ? " )
119+ }
120+
121+ /// Native SwiftUI redraw of the brand logo. We rasterise from PNG assets
122+ /// for the AppIcon (Apple requires `.icns` from PNGs there), but for
123+ /// in-app surfaces the bitmap import introduces a faint halo at the
124+ /// rounded-square edges from anti-aliasing. Drawing as live shapes
125+ /// stays crisp at any scale and ditches the halo entirely.
111126struct BrandMark : View {
112- /// Defaults to the sidebar's 26 pt; callers like `Onboarding` scale via
113- /// `.scaleEffect()` so we don't need a `size` parameter.
127+ /// Defaults to the sidebar's 26 pt. `Onboarding` scales via
128+ /// `.scaleEffect()` so the geometry inside remains pixel-aligned.
129+ var size : CGFloat = 26
130+
131+ private var corner : CGFloat { size * ( 232.0 / 1024.0 ) }
132+
114133 var body : some View {
115- Image ( " BrandLogo " )
116- . resizable ( )
117- . interpolation ( . high)
118- . frame ( width: 26 , height: 26 )
119- . shadow ( color: Color ( hex: 0x0A84FF ) . opacity ( 0.32 ) , radius: 4 , x: 0 , y: 2 )
134+ ZStack {
135+ // Badge gradient — matches Logo.svg's #0A84FF → #5E5CE6 stops.
136+ RoundedRectangle ( cornerRadius: corner, style: . continuous)
137+ . fill ( LinearGradient ( colors: [ Color ( hex: 0x0A84FF ) , Color ( hex: 0x5E5CE6 ) ] ,
138+ startPoint: . topLeading, endPoint: . bottomTrailing) )
139+ // Subtle radial highlight near the top-left so the badge has dimension.
140+ RoundedRectangle ( cornerRadius: corner, style: . continuous)
141+ . fill ( RadialGradient ( colors: [ . white. opacity ( 0.20 ) , . clear] ,
142+ center: UnitPoint ( x: 0.28 , y: 0.22 ) ,
143+ startRadius: 0 , endRadius: size * 0.95 ) )
144+ // Hairline inner stroke — matches the SVG's white-12% inset rect.
145+ RoundedRectangle ( cornerRadius: corner - 0.5 , style: . continuous)
146+ . stroke ( . white. opacity ( 0.12 ) , lineWidth: 1 )
147+ . padding ( 0.5 )
148+ // Stylized "B" — vertical stem + two stacked D-loops, baked
149+ // into one path so it fills as one shape. Coords are
150+ // normalised against the SVG's 1024-unit canvas, then scaled
151+ // to the runtime size.
152+ BLetterShape ( )
153+ . fill ( LinearGradient (
154+ colors: [ . white, Color ( hex: 0xF0E9FF ) ] ,
155+ startPoint: UnitPoint ( x: 0.3 , y: 0.05 ) ,
156+ endPoint: UnitPoint ( x: 0.6 , y: 1.0 )
157+ ) )
158+ // Smart-care sparkle — fades out below the sidebar threshold so
159+ // it doesn't read as noise at 26 pt.
160+ if size >= 36 {
161+ SparkleShape ( )
162+ . fill ( . white. opacity ( 0.94 ) )
163+ . frame ( width: size * 0.16 , height: size * 0.16 )
164+ . offset ( x: size * 0.275 , y: - size * 0.275 )
165+ }
166+ }
167+ . frame ( width: size, height: size)
168+ . compositingGroup ( )
169+ . shadow ( color: Color ( hex: 0x0A84FF ) . opacity ( 0.32 ) , radius: size * 0.18 , x: 0 , y: size * 0.08 )
170+ }
171+ }
172+
173+ /// Filled "B": vertical stem + two D-loops in a single path. All
174+ /// coordinates expressed against the SVG's 1024-unit canvas, then
175+ /// scaled to the runtime rect.
176+ private struct BLetterShape : Shape {
177+ func path( in rect: CGRect ) -> Path {
178+ let scale = rect. width / 1024.0
179+ func p( _ x: CGFloat , _ y: CGFloat ) -> CGPoint {
180+ CGPoint ( x: rect. minX + x * scale, y: rect. minY + y * scale)
181+ }
182+ var path = Path ( )
183+ // Stem: rounded rect 288..394 × 268..756.
184+ path. addRoundedRect (
185+ in: CGRect ( origin: p ( 288 , 268 ) ,
186+ size: CGSize ( width: 106 * scale, height: 488 * scale) ) ,
187+ cornerSize: CGSize ( width: 22 * scale, height: 22 * scale)
188+ )
189+ // Top D-loop: from (390,268) down to (390,504), arc back via right bulge.
190+ path. move ( to: p ( 390 , 268 ) )
191+ path. addLine ( to: p ( 390 , 504 ) )
192+ path. addArc (
193+ tangent1End: p ( 514 , 504 ) ,
194+ tangent2End: p ( 514 , 268 ) ,
195+ radius: 124 * scale
196+ )
197+ path. addArc (
198+ tangent1End: p ( 514 , 268 ) ,
199+ tangent2End: p ( 390 , 268 ) ,
200+ radius: 124 * scale
201+ )
202+ path. closeSubpath ( )
203+ // Bottom D-loop: slightly larger per typographic convention.
204+ path. move ( to: p ( 390 , 520 ) )
205+ path. addLine ( to: p ( 390 , 756 ) )
206+ path. addArc (
207+ tangent1End: p ( 528 , 756 ) ,
208+ tangent2End: p ( 528 , 520 ) ,
209+ radius: 138 * scale
210+ )
211+ path. addArc (
212+ tangent1End: p ( 528 , 520 ) ,
213+ tangent2End: p ( 390 , 520 ) ,
214+ radius: 138 * scale
215+ )
216+ path. closeSubpath ( )
217+ return path
218+ }
219+ }
220+
221+ /// Four-point sparkle: a vertical bar + horizontal bar (rounded) + a
222+ /// faint 45°-rotated rounded square between them.
223+ private struct SparkleShape : Shape {
224+ func path( in rect: CGRect ) -> Path {
225+ var path = Path ( )
226+ let cx = rect. midX, cy = rect. midY
227+ let arm = rect. width * 0.5
228+ let thick = rect. width * 0.16
229+ // Vertical bar
230+ path. addRoundedRect (
231+ in: CGRect ( x: cx - thick / 2 , y: cy - arm, width: thick, height: arm * 2 ) ,
232+ cornerSize: CGSize ( width: thick / 2 , height: thick / 2 )
233+ )
234+ // Horizontal bar
235+ path. addRoundedRect (
236+ in: CGRect ( x: cx - arm, y: cy - thick / 2 , width: arm * 2 , height: thick) ,
237+ cornerSize: CGSize ( width: thick / 2 , height: thick / 2 )
238+ )
239+ return path
120240 }
121241}
122242
0 commit comments