Guide for adding new report cards to ClaudeBar that analyze local data sources and display metrics with comparison deltas. Use this skill when: (1) Adding a new report/analytics card (e.g., weekly summary, model breakdown, session stats) (2) Creating data analysis features that read local files and display aggregated metrics (3) Adding comparison cards that show "today vs previous" style deltas (4) Building any feature that follows the DailyUsage pattern (parse → aggregate → report → card)
75
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Add new report cards that analyze local data sources, compute metrics with comparison deltas, and display them in the existing card UI style using TDD.
This skill covers adding report-style features — cards that:
Every report feature follows this data flow:
Data Source → Parser → Analyzer → Report Model → UsageSnapshot → Card ViewMapped to the codebase layers:
| Layer | Location | What to Create |
|---|---|---|
| Domain | Sources/Domain/{Feature}/ | Rich models + @Mockable protocol |
| Infrastructure | Sources/Infrastructure/{Provider}/ | Parser + Analyzer implementation |
| App | Sources/App/Views/ | Card view(s) |
| Integration | Provider class + statsGrid | Wire analyzer → snapshot → UI |
Reference implementation: See
references/daily-usage-pattern.mdfor the complete DailyUsage feature as a working example of this pattern.
Phase 0: Architecture Design (get user approval)
↓
Phase 1: Domain Models + Tests (TDD Red→Green)
↓
Phase 2: Infrastructure Parser + Analyzer + Tests
↓
Phase 3: Card View + Integration
↓
Phase 4: Verify all tests passBefore writing code, design the feature and get user approval.
Answer these questions:
Example: Adding a weekly cost breakdown report
┌─────────────────────────────────────────────────────────┐
│ Data Source Infrastructure Domain │
│ │
│ ~/.claude/ → WeeklyParser → WeeklyStat │
│ projects/ (reads JSONL) (per-day cost) │
│ *.jsonl │
│ WeeklyAnalyzer → WeeklyReport │
│ (aggregates by (this week vs │
│ week, implements last week) │
│ protocol) │
│ ↓ ↓ │
│ Provider.refresh() → UsageSnapshot │
│ .weeklyReport │
│ ↓ │
│ statsGrid() → WeeklyCardView │
└─────────────────────────────────────────────────────────┘| Component | Purpose | Inputs | Outputs |
|---|---|---|---|
{Name}Stat | Single period's data | Raw aggregated values | Formatted strings, isEmpty |
{Name}Report | Period comparison | Two Stats | Deltas, percentages, progress |
{Name}Analyzing | Protocol | Date/config | Report |
{Name}Analyzer | Implementation | File paths | Report |
{Name}CardView | UI card | Report + metric | Glassmorphism card |
Use AskUserQuestion to confirm the design before proceeding.
The stat model represents one period's aggregated data with rich formatting behavior.
Location: Sources/Domain/{Feature}/{Name}Stat.swift
Pattern to follow:
import Foundation
public struct {Name}Stat: Sendable, Equatable {
public let date: Date
// Add your metrics here
public let metricA: Decimal
public let metricB: Int
// Formatting — encapsulate in the model, not the view
public var formattedMetricA: String { /* currency, compact number, duration, etc. */ }
public var formattedMetricB: String { /* ... */ }
public var isEmpty: Bool { /* all zeros check */ }
public static func empty(for date: Date) -> Self { /* zero-valued instance */ }
}Key rules:
Decimal for monetary values (not Double — floating point errors)TimeInterval for durationsLocale(identifier: "en_US") for currency formatting (not en_US_POSIX which adds a space)isEmpty uses && (all zeros = empty), not ||The report model compares two periods and computes deltas.
Location: Sources/Domain/{Feature}/{Name}Report.swift
Pattern to follow:
public struct {Name}Report: Sendable, Equatable {
public let current: {Name}Stat // e.g., today, this week
public let previous: {Name}Stat // e.g., yesterday, last week
// Delta calculations
public var metricADelta: Decimal { current.metricA - previous.metricA }
public var metricAChangePercent: Double? {
guard previous.metricA > 0 else { return nil } // nil when previous is zero
// ...
}
// Formatted deltas with sign: "+$5.00", "-1.2M"
public var formattedMetricADelta: String { /* ... */ }
// Progress for bar display (0-1 ratio of current vs total)
public var metricAProgress: Double {
let total = /* current + previous */
guard total > 0 else { return 0 }
return current / total
}
}Key rules:
nil when previous is zero (avoid division by zero)+ or -)current / (current + previous), clamped to 0-1abs() for formatted values, prepend sign separatelyLocation: Sources/Domain/{Feature}/{Name}Analyzing.swift
import Mockable
@Mockable
public protocol {Name}Analyzing: Sendable {
func analyze() async throws -> {Name}Report
}Location: Tests/DomainTests/{Feature}/
Create two test files following Chicago School TDD (test state, not interactions):
{Name}StatTests.swift — Test formatting, isEmpty, edge cases{Name}ReportTests.swift — Test deltas, percentages, nil cases, progressimport Foundation
import Testing
@testable import Domain
@Suite
struct {Name}StatTests {
@Test func `formats metric as expected`() {
let stat = {Name}Stat(date: Date(), metricA: 14.26, ...)
#expect(stat.formattedMetricA == "$14.26")
}
// ...
}After writing tests → implement the models → run tests → all green.
If the report reads local files (JSONL, JSON, CSV), create a parser.
Location: Sources/Infrastructure/{Provider}/{Name}Parser.swift
Pattern: Parser is a struct (not protocol) since it's a pure data transformation.
struct {Name}Parser {
func parse(fileURL: URL) throws -> [{Name}Record] { /* ... */ }
func parse(content: String) -> [{Name}Record] { /* for testing */ }
}Location: Sources/Infrastructure/{Provider}/{Provider}{Name}Analyzer.swift
The analyzer implements the domain protocol and orchestrates:
Performance rule: Only scan files modified within the relevant time window. With 2000+ JSONL files, scanning all of them is too slow.
public struct {Provider}{Name}Analyzer: {Name}Analyzing, Sendable {
public func analyze() async throws -> {Name}Report {
let files = findRecentFiles(since: periodStart) // Performance!
// parse → partition → aggregate → return
}
}Location: Tests/InfrastructureTests/{Provider}/
Location: Sources/App/Views/{Name}CardView.swift
The card must match the existing glassmorphism style. Use WrappedStatCard as the reference:
struct {Name}CardView: View {
let metric: {Name}Metric // enum for each displayable metric
let report: {Name}Report
let delay: Double // for cascading entrance animation
@Environment(\.appTheme) private var theme
var body: some View {
VStack(alignment: .leading, spacing: 6) {
// 1. Header: icon + LABEL (uppercased)
// 2. Large value (e.g., "$14.26" or "19.5M")
// 3. Progress bar (animated)
// 4. Delta comparison line (e.g., "Vs Mar 10 -$27.47 (4.9%)")
}
.padding(12)
.background(/* theme.cardGradient + theme.glassBorder stroke */)
.scaleEffect(isHovering ? 1.015 : 1.0)
.onHover { isHovering = $0 }
}
}Card styling checklist:
.padding(12) on the VStacktheme.cardGradient fill + theme.glassBorder stroke (1pt)theme.cardCornerRadius for cornerstheme.fontDesign on all texttheme.textPrimary / theme.textSecondary / theme.textTertiary for text hierarchytheme.progressTrack for bar backgrounddelay parameterAdd an optional field for the report:
// In Sources/Domain/Provider/UsageSnapshot.swift
public let {name}Report: {Name}Report?
// Add to init with default nilInject the analyzer into the provider that owns this data:
// In the provider's init:
private let {name}Analyzer: (any {Name}Analyzing)?
// In refresh():
snapshot = await attach{Name}Report(to: newSnapshot)
// Helper method:
private func attach{Name}Report(to snapshot: UsageSnapshot) async -> UsageSnapshot {
guard let analyzer = {name}Analyzer,
let report = try? await analyzer.analyze(),
!report.current.isEmpty else { return snapshot }
return UsageSnapshot(/* copy all fields, add report */)
}Add to MenuContentView.statsGrid(snapshot:):
if let report = snapshot.{name}Report {
let baseDelay = Double(snapshot.quotas.count + 1) * 0.08
// Render card(s) in LazyVGrid or standalone
}Pass the analyzer when creating the provider:
{Provider}Provider(
probe: ...,
settingsRepository: settingsRepository,
{name}Analyzer: {Provider}{Name}Analyzer()
)tuist generatexcodebuild test -scheme ClaudeBar-Workspace ...{Name}StatTests (formatting, isEmpty, edge cases){Name}Stat — make tests green{Name}ReportTests (deltas, percentages, progress){Name}Report — make tests green{Name}Analyzing protocol with @Mockable{Name}CardView matching glassmorphism style{name}Report field to UsageSnapshotrefresh() via attach{Name}ReportstatsGridClaudeBarApptuist generate succeeds92b9351
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.