mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
feat(dashboards): v2.7 widget catalog — file-reading widgets, sparkline, typed status, project-wide watch
Major project-dashboard release. Five new widget types (markdown_file, log_tail, cron_status, image, status_grid), inline sparkline on stat, typed status enum shared by list + status_grid, structured WidgetErrorCard, and a project-wide .scarf/ directory watch that picks up files cron jobs write next to dashboard.json. - ProjectDashboard: extend DashboardWidget with path/lines/jobId/cells/gridColumns/sparkline; add StatusGridCell + ListItemStatus (lenient parse with synonyms) - HermesFileWatcher: watch each project's .scarf/ dir alongside dashboard.json (local FSEvents + remote SSH mtime poll); updateProjectWatches signature now takes dashboardPaths + scarfDirs - New widget views: CronStatus, Image, LogTail, MarkdownFile, StatusGrid, plus WidgetErrorCard for structured failure messaging; legacy "Unknown" placeholder replaced everywhere - WidgetPathResolver: project-root-anchored path resolution that rejects absolute paths + ".." escapes pre and post canonicalization - Stat widget gains optional inline sparkline (pure SwiftUI Path, no Charts dep); list widget rows route through typed status with semantic icons + ScarfColor tints - iOS list widget + unsupported card adopt typed status + warning-toned error card (parity with Mac error styling); new widget types remain Mac-only - Site mirror: widgets.js renders all five new types (file-reading widgets show annotated catalog placeholders), sparkline SVG, status-grid grid; styles.css adds typed-status palette + error-card + sparkline + grid styles - Catalog validator: tools/widget-schema.json is the single source of truth; build-catalog.py loads it and enforces per-type required fields. 8 new test cases in test_build_catalog.py covering schema load, v2.7 additions, and missing-required rejection - Template-author skill (SKILL.md) gains v2.7 Widget Catalog section + canonical status guidance; CONTRIBUTING.md points authors at widget-schema.json; template-author bundle rebuilt - Localizable.xcstrings picks up auto-extracted strings for the previously-shipped OAuth keepalive feature - Release notes drafted at releases/v2.7.0/RELEASE_NOTES.md Backwards compatible — existing dashboard.json renders byte-identically, status synonyms (ok/up/down/active/etc.) keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,17 +102,31 @@ struct WidgetView: View {
|
||||
}
|
||||
|
||||
private var unsupportedView: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Label(widget.title, systemImage: "questionmark.app.dashed")
|
||||
.font(.caption)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
Text("Widget type \"\(widget.type)\" isn't supported in this version of Scarf yet.")
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
Text(widget.title.isEmpty ? "Widget error" : widget.title)
|
||||
.font(.caption)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
}
|
||||
Text("Unknown widget type: \"\(widget.type)\"")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Text("This Scarf build doesn't render this widget type. Update Scarf or change the widget type in dashboard.json.")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(12)
|
||||
.background(.quaternary.opacity(0.5))
|
||||
.background(ScarfColor.warning.opacity(0.08))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(ScarfColor.warning.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,7 @@ struct ListWidgetView: View {
|
||||
}
|
||||
if let items = widget.items {
|
||||
ForEach(items) { item in
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: statusIcon(item.status))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(statusColor(item.status))
|
||||
Text(item.text)
|
||||
.font(.callout)
|
||||
.strikethrough(item.status == "done")
|
||||
.foregroundStyle(item.status == "done" ? .secondary : .primary)
|
||||
}
|
||||
ListItemRow(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,21 +28,52 @@ struct ListWidgetView: View {
|
||||
.background(.quaternary.opacity(0.5))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
private func statusIcon(_ status: String?) -> String {
|
||||
switch status {
|
||||
case "done": return "checkmark.circle.fill"
|
||||
case "active": return "circle.inset.filled"
|
||||
case "pending": return "circle"
|
||||
default: return "circle"
|
||||
private struct ListItemRow: View {
|
||||
let item: ListItem
|
||||
|
||||
private var typedStatus: ListItemStatus? { ListItemStatus(raw: item.status) }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: iconName)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(tint)
|
||||
Text(item.text)
|
||||
.font(.callout)
|
||||
.strikethrough(typedStatus == .done)
|
||||
.foregroundStyle(typedStatus == .done ? .secondary : .primary)
|
||||
if typedStatus == nil, let raw = item.status, !raw.isEmpty {
|
||||
Text(raw)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(.quaternary.opacity(0.5))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func statusColor(_ status: String?) -> Color {
|
||||
switch status {
|
||||
case "done": return .green
|
||||
case "active": return .blue
|
||||
default: return .secondary
|
||||
private var iconName: String {
|
||||
switch typedStatus {
|
||||
case .success, .done: return "checkmark.circle.fill"
|
||||
case .warning: return "exclamationmark.triangle.fill"
|
||||
case .danger: return "xmark.octagon.fill"
|
||||
case .info: return "info.circle.fill"
|
||||
case .pending: return "circle.dashed"
|
||||
case .neutral, nil: return "circle"
|
||||
}
|
||||
}
|
||||
|
||||
private var tint: Color {
|
||||
switch typedStatus {
|
||||
case .success, .done: return ScarfColor.success
|
||||
case .warning: return ScarfColor.warning
|
||||
case .danger: return ScarfColor.danger
|
||||
case .info: return ScarfColor.info
|
||||
case .pending, .neutral, nil: return .secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user