Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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)
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 121% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 237% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 215% | 0% |
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.md for 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:
swiftimport 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:
swiftpublic 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
swiftimport 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, progressswiftimport 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.
swiftstruct {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.
swiftpublic 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:
swiftstruct {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:
swift// In Sources/Domain/Provider/UsageSnapshot.swift public let {name}Report: {Name}Report? // Add to init with default nil
Inject the analyzer into the provider that owns this data:
swift// 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:):
swiftif 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:
swift{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 succeedsOther measured skills in the registry, with their headline benchmark lift.