Monorepo for Tangled tangled.org

appview/ogcard: introduce highlevel helpers to draw icons and assets

- DrawLucideIcon & DrawDollySilhouette to simplify the svg drawing logic
- DrawDollySilhouette now depends on the html template itself, instead
of a bespoke svg

this changes lets us remove dolly.svg from the fragments.

Signed-off-by: oppiliappan <me@oppi.li>

Changed files
+75 -26
appview
issues
ogcard
pulls
repo
+5 -5
appview/issues/opengraph.go
··· 143 var statusBgColor color.RGBA 144 145 if issue.Open { 146 - statusIcon = "static/icons/circle-dot.svg" 147 statusText = "open" 148 statusBgColor = color.RGBA{34, 139, 34, 255} // green 149 } else { 150 - statusIcon = "static/icons/circle-dot.svg" 151 statusText = "closed" 152 statusBgColor = color.RGBA{52, 58, 64, 255} // dark gray 153 } ··· 155 badgeIconSize := 36 156 157 // Draw icon with status color (no background) 158 - err = statusCommentsArea.DrawSVGIcon(statusIcon, statsX, statsY+iconBaselineOffset-badgeIconSize/2+5, badgeIconSize, statusBgColor) 159 if err != nil { 160 log.Printf("failed to draw status icon: %v", err) 161 } ··· 172 currentX := statsX + badgeIconSize + 12 + statusTextWidth + 50 173 174 // Draw comment count 175 - err = statusCommentsArea.DrawSVGIcon("static/icons/message-square.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 176 if err != nil { 177 log.Printf("failed to draw comment icon: %v", err) 178 } ··· 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 - err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 197 if err != nil { 198 log.Printf("dolly silhouette not available (this is ok): %v", err) 199 }
··· 143 var statusBgColor color.RGBA 144 145 if issue.Open { 146 + statusIcon = "circle-dot" 147 statusText = "open" 148 statusBgColor = color.RGBA{34, 139, 34, 255} // green 149 } else { 150 + statusIcon = "ban" 151 statusText = "closed" 152 statusBgColor = color.RGBA{52, 58, 64, 255} // dark gray 153 } ··· 155 badgeIconSize := 36 156 157 // Draw icon with status color (no background) 158 + err = statusCommentsArea.DrawLucideIcon(statusIcon, statsX, statsY+iconBaselineOffset-badgeIconSize/2+5, badgeIconSize, statusBgColor) 159 if err != nil { 160 log.Printf("failed to draw status icon: %v", err) 161 } ··· 172 currentX := statsX + badgeIconSize + 12 + statusTextWidth + 50 173 174 // Draw comment count 175 + err = statusCommentsArea.DrawLucideIcon("message-square", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 176 if err != nil { 177 log.Printf("failed to draw comment icon: %v", err) 178 } ··· 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 + err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 197 if err != nil { 198 log.Printf("dolly silhouette not available (this is ok): %v", err) 199 }
+59 -10
appview/ogcard/card.go
··· 7 import ( 8 "bytes" 9 "fmt" 10 "image" 11 "image/color" 12 "io" ··· 279 return width, nil 280 } 281 282 - // DrawSVGIcon draws an SVG icon from the embedded files at the specified position 283 - func (c *Card) DrawSVGIcon(svgPath string, x, y, size int, iconColor color.Color) error { 284 - svgData, err := pages.Files.ReadFile(svgPath) 285 - if err != nil { 286 - return fmt.Errorf("failed to read SVG file %s: %w", svgPath, err) 287 - } 288 - 289 // Convert color to hex string for SVG 290 rgba, isRGBA := iconColor.(color.RGBA) 291 if !isRGBA { ··· 304 // Parse SVG 305 icon, err := oksvg.ReadIconStream(strings.NewReader(svgString)) 306 if err != nil { 307 - return fmt.Errorf("failed to parse SVG %s: %w", svgPath, err) 308 } 309 310 // Set the icon size 311 w, h := float64(size), float64(size) 312 icon.SetTarget(0, 0, w, h) ··· 334 } 335 336 draw.Draw(c.Img, destRect, iconImg, image.Point{}, draw.Over) 337 - 338 - return nil 339 } 340 341 // DrawImage fills the card with an image, scaled to maintain the original aspect ratio and centered with respect to the non-filled dimension
··· 7 import ( 8 "bytes" 9 "fmt" 10 + "html/template" 11 "image" 12 "image/color" 13 "io" ··· 280 return width, nil 281 } 282 283 + func BuildSVGIconFromData(svgData []byte, iconColor color.Color) (*oksvg.SvgIcon, error) { 284 // Convert color to hex string for SVG 285 rgba, isRGBA := iconColor.(color.RGBA) 286 if !isRGBA { ··· 299 // Parse SVG 300 icon, err := oksvg.ReadIconStream(strings.NewReader(svgString)) 301 if err != nil { 302 + return nil, fmt.Errorf("failed to parse SVG: %w", err) 303 } 304 305 + return icon, nil 306 + } 307 + 308 + func BuildSVGIconFromPath(svgPath string, iconColor color.Color) (*oksvg.SvgIcon, error) { 309 + svgData, err := pages.Files.ReadFile(svgPath) 310 + if err != nil { 311 + return nil, fmt.Errorf("failed to read SVG file %s: %w", svgPath, err) 312 + } 313 + 314 + icon, err := BuildSVGIconFromData(svgData, iconColor) 315 + if err != nil { 316 + return nil, fmt.Errorf("failed to build SVG icon %s: %w", svgPath, err) 317 + } 318 + 319 + return icon, nil 320 + } 321 + 322 + func BuildLucideIcon(name string, iconColor color.Color) (*oksvg.SvgIcon, error) { 323 + return BuildSVGIconFromPath(fmt.Sprintf("static/icons/%s.svg", name), iconColor) 324 + } 325 + 326 + func (c *Card) DrawLucideIcon(name string, x, y, size int, iconColor color.Color) error { 327 + icon, err := BuildSVGIconFromPath(fmt.Sprintf("static/icons/%s.svg", name), iconColor) 328 + if err != nil { 329 + return err 330 + } 331 + 332 + c.DrawSVGIcon(icon, x, y, size) 333 + 334 + return nil 335 + } 336 + 337 + func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error { 338 + tpl, err := template.New("dolly"). 339 + ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html") 340 + if err != nil { 341 + return fmt.Errorf("failed to read dolly silhouette template: %w", err) 342 + } 343 + 344 + var svgData bytes.Buffer 345 + if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil { 346 + return fmt.Errorf("failed to execute dolly silhouette template: %w", err) 347 + } 348 + 349 + icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor) 350 + if err != nil { 351 + return err 352 + } 353 + 354 + c.DrawSVGIcon(icon, x, y, size) 355 + 356 + return nil 357 + } 358 + 359 + // DrawSVGIcon draws an SVG icon from the embedded files at the specified position 360 + func (c *Card) DrawSVGIcon(icon *oksvg.SvgIcon, x, y, size int) { 361 // Set the icon size 362 w, h := float64(size), float64(size) 363 icon.SetTarget(0, 0, w, h) ··· 385 } 386 387 draw.Draw(c.Img, destRect, iconImg, image.Point{}, draw.Over) 388 } 389 390 // DrawImage fills the card with an image, scaled to maintain the original aspect ratio and centered with respect to the non-filled dimension
+7 -7
appview/pulls/opengraph.go
··· 146 var statusColor color.RGBA 147 148 if pull.State.IsOpen() { 149 - statusIcon = "static/icons/git-pull-request.svg" 150 statusText = "open" 151 statusColor = color.RGBA{34, 139, 34, 255} // green 152 } else if pull.State.IsMerged() { 153 - statusIcon = "static/icons/git-merge.svg" 154 statusText = "merged" 155 statusColor = color.RGBA{138, 43, 226, 255} // purple 156 } else { 157 - statusIcon = "static/icons/git-pull-request-closed.svg" 158 statusText = "closed" 159 statusColor = color.RGBA{128, 128, 128, 255} // gray 160 } ··· 162 statusIconSize := 36 163 164 // Draw icon with status color 165 - err = statusStatsArea.DrawSVGIcon(statusIcon, statsX, statsY+iconBaselineOffset-statusIconSize/2+5, statusIconSize, statusColor) 166 if err != nil { 167 log.Printf("failed to draw status icon: %v", err) 168 } ··· 179 currentX := statsX + statusIconSize + 12 + statusTextWidth + 40 180 181 // Draw comment count 182 - err = statusStatsArea.DrawSVGIcon("static/icons/message-square.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 183 if err != nil { 184 log.Printf("failed to draw comment icon: %v", err) 185 } ··· 198 currentX += commentTextWidth + 40 199 200 // Draw files changed 201 - err = statusStatsArea.DrawSVGIcon("static/icons/file-diff.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 202 if err != nil { 203 log.Printf("failed to draw file diff icon: %v", err) 204 } ··· 241 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 242 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 243 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 244 - err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 245 if err != nil { 246 log.Printf("dolly silhouette not available (this is ok): %v", err) 247 }
··· 146 var statusColor color.RGBA 147 148 if pull.State.IsOpen() { 149 + statusIcon = "git-pull-request" 150 statusText = "open" 151 statusColor = color.RGBA{34, 139, 34, 255} // green 152 } else if pull.State.IsMerged() { 153 + statusIcon = "git-merge" 154 statusText = "merged" 155 statusColor = color.RGBA{138, 43, 226, 255} // purple 156 } else { 157 + statusIcon = "git-pull-request-closed" 158 statusText = "closed" 159 statusColor = color.RGBA{128, 128, 128, 255} // gray 160 } ··· 162 statusIconSize := 36 163 164 // Draw icon with status color 165 + err = statusStatsArea.DrawLucideIcon(statusIcon, statsX, statsY+iconBaselineOffset-statusIconSize/2+5, statusIconSize, statusColor) 166 if err != nil { 167 log.Printf("failed to draw status icon: %v", err) 168 } ··· 179 currentX := statsX + statusIconSize + 12 + statusTextWidth + 40 180 181 // Draw comment count 182 + err = statusStatsArea.DrawLucideIcon("message-square", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 183 if err != nil { 184 log.Printf("failed to draw comment icon: %v", err) 185 } ··· 198 currentX += commentTextWidth + 40 199 200 // Draw files changed 201 + err = statusStatsArea.DrawLucideIcon("static/icons/file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 202 if err != nil { 203 log.Printf("failed to draw file diff icon: %v", err) 204 } ··· 241 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 242 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 243 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 244 + err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 245 if err != nil { 246 log.Printf("dolly silhouette not available (this is ok): %v", err) 247 }
+4 -4
appview/repo/opengraph.go
··· 158 // Draw star icon, count, and label 159 // Align icon baseline with text baseline 160 iconBaselineOffset := int(textSize) / 2 161 - err = statsArea.DrawSVGIcon("static/icons/star.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 162 if err != nil { 163 log.Printf("failed to draw star icon: %v", err) 164 } ··· 185 186 // Draw issues icon, count, and label 187 issueStartX := currentX 188 - err = statsArea.DrawSVGIcon("static/icons/circle-dot.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 189 if err != nil { 190 log.Printf("failed to draw circle-dot icon: %v", err) 191 } ··· 210 211 // Draw pull request icon, count, and label 212 prStartX := currentX 213 - err = statsArea.DrawSVGIcon("static/icons/git-pull-request.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 214 if err != nil { 215 log.Printf("failed to draw git-pull-request icon: %v", err) 216 } ··· 236 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 237 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 238 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 239 - err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 240 if err != nil { 241 log.Printf("dolly silhouette not available (this is ok): %v", err) 242 }
··· 158 // Draw star icon, count, and label 159 // Align icon baseline with text baseline 160 iconBaselineOffset := int(textSize) / 2 161 + err = statsArea.DrawLucideIcon("star", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 162 if err != nil { 163 log.Printf("failed to draw star icon: %v", err) 164 } ··· 185 186 // Draw issues icon, count, and label 187 issueStartX := currentX 188 + err = statsArea.DrawLucideIcon("circle-dot", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 189 if err != nil { 190 log.Printf("failed to draw circle-dot icon: %v", err) 191 } ··· 210 211 // Draw pull request icon, count, and label 212 prStartX := currentX 213 + err = statsArea.DrawLucideIcon("git-pull-request", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 214 if err != nil { 215 log.Printf("failed to draw git-pull-request icon: %v", err) 216 } ··· 236 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 237 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 238 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 239 + err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 240 if err != nil { 241 log.Printf("dolly silhouette not available (this is ok): %v", err) 242 }