renderStoriesPanel renders the stories list panel.
(width, height int)
| 367 | |
| 368 | // renderStoriesPanel renders the stories list panel. |
| 369 | func (a *App) renderStoriesPanel(width, height int) string { |
| 370 | var content strings.Builder |
| 371 | |
| 372 | // Panel title — append scroll percentage when list is scrollable |
| 373 | listHeight := height - 5 // Account for title, border, and progress bar |
| 374 | totalStories := len(a.prd.UserStories) |
| 375 | titleText := "Stories" |
| 376 | if totalStories > listHeight && listHeight > 0 { |
| 377 | maxOffset := totalStories - listHeight |
| 378 | pct := 0 |
| 379 | if maxOffset > 0 { |
| 380 | pct = a.storiesScrollOffset * 100 / maxOffset |
| 381 | } |
| 382 | titleText = fmt.Sprintf("Stories (%d%%)", pct) |
| 383 | } |
| 384 | title := PanelTitleStyle.Render(titleText) |
| 385 | content.WriteString(title) |
| 386 | content.WriteString("\n") |
| 387 | content.WriteString(DividerStyle.Render(strings.Repeat("─", width-2))) |
| 388 | content.WriteString("\n") |
| 389 | |
| 390 | // Clamp scroll offset |
| 391 | if a.storiesScrollOffset < 0 { |
| 392 | a.storiesScrollOffset = 0 |
| 393 | } |
| 394 | if listHeight > 0 && a.storiesScrollOffset > totalStories-listHeight { |
| 395 | a.storiesScrollOffset = totalStories - listHeight |
| 396 | } |
| 397 | if a.storiesScrollOffset < 0 { |
| 398 | a.storiesScrollOffset = 0 |
| 399 | } |
| 400 | |
| 401 | // Render visible slice of stories |
| 402 | endIdx := a.storiesScrollOffset + listHeight |
| 403 | if endIdx > totalStories { |
| 404 | endIdx = totalStories |
| 405 | } |
| 406 | visibleCount := 0 |
| 407 | for i := a.storiesScrollOffset; i < endIdx; i++ { |
| 408 | story := a.prd.UserStories[i] |
| 409 | icon := GetStatusIcon(story.Passes, story.InProgress) |
| 410 | |
| 411 | // Truncate title to fit |
| 412 | maxTitleLen := width - 12 // Account for icon, ID, and spacing |
| 413 | displayTitle := story.Title |
| 414 | if len(displayTitle) > maxTitleLen && maxTitleLen > 3 { |
| 415 | displayTitle = displayTitle[:maxTitleLen-3] + "..." |
| 416 | } |
| 417 | |
| 418 | line := fmt.Sprintf("%s %s %s", icon, story.ID, displayTitle) |
| 419 | |
| 420 | if i == a.selectedIndex { |
| 421 | // Pad line to full width to ensure background fills the entire row |
| 422 | lineWidth := lipgloss.Width(line) |
| 423 | targetWidth := width - 2 |
| 424 | if lineWidth < targetWidth { |
| 425 | line = line + strings.Repeat(" ", targetWidth-lineWidth) |
| 426 | } |