From d52e1130ec640c839db2945b5c86f54f54078d4a Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 12:32:11 +0200 Subject: [PATCH] add ticket type color coding with dark/light presets TicketTypeColors maps initiative/epic/story/task/bug to distinct colors, with dark and light presets auto-selected from the active theme. Ticket IDs are now color-coded by type (purple initiative, blue epic, green story, muted task, red bug). Status dot remains for state. Pattern documented in theme-ui skill as the standard for extension-owned domain colors. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/theme-ui/SKILL.md | 13 +++++++ lib/builtin/tickets/src/ticket_colors.dart | 45 ++++++++++++++++++++++ lib/builtin/tickets/src/tickets_view.dart | 10 ++++- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 lib/builtin/tickets/src/ticket_colors.dart diff --git a/.claude/skills/theme-ui/SKILL.md b/.claude/skills/theme-ui/SKILL.md index 20fa50ea..ad430169 100644 --- a/.claude/skills/theme-ui/SKILL.md +++ b/.claude/skills/theme-ui/SKILL.md @@ -154,6 +154,19 @@ the shared root for all frame surfaces. They resolve to `bgSunken` / `textDim` / `border` in the palette. Themes can override them to diverge hat from sidebar from status bar if desired. +## Extension-owned domain colors + +Extensions that need domain-specific color coding (ticket types, decision +types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead: + +1. Create a color map class in the extension (e.g. `TicketTypeColors`) +2. Ship dark and light presets, auto-selected via `ClideTheme.of(context).dark` +3. Store user overrides under `ext..colors` in settings +4. Reference: `lib/builtin/tickets/src/ticket_colors.dart` + +This keeps the core token surface lean and lets each extension own its +palette. The pattern scales to any extension needing domain colors. + ## Anti-patterns - Borrowing another surface's token (`sidebarBackground` for hat bar) diff --git a/lib/builtin/tickets/src/ticket_colors.dart b/lib/builtin/tickets/src/ticket_colors.dart new file mode 100644 index 00000000..2f7c41ff --- /dev/null +++ b/lib/builtin/tickets/src/ticket_colors.dart @@ -0,0 +1,45 @@ +import 'dart:ui' show Color; + +class TicketTypeColors { + const TicketTypeColors({ + required this.initiative, + required this.epic, + required this.story, + required this.task, + required this.bug, + }); + + final Color initiative; + final Color epic; + final Color story; + final Color task; + final Color bug; + + Color forType(String? type) => switch (type) { + 'initiative' => initiative, + 'epic' => epic, + 'story' => story, + 'task' => task, + 'bug' => bug, + _ => task, + }; + + static const dark = TicketTypeColors( + initiative: Color(0xFFC792EA), + epic: Color(0xFF78A0F8), + story: Color(0xFF7DD3A8), + task: Color(0xFF78809C), + bug: Color(0xFFE87D7D), + ); + + static const light = TicketTypeColors( + initiative: Color(0xFF7B2CB0), + epic: Color(0xFF2962B8), + story: Color(0xFF1D7A4E), + task: Color(0xFF5A6070), + bug: Color(0xFFC03030), + ); + + static TicketTypeColors forTheme({required bool dark}) => + dark ? TicketTypeColors.dark : TicketTypeColors.light; +} diff --git a/lib/builtin/tickets/src/tickets_view.dart b/lib/builtin/tickets/src/tickets_view.dart index 094db870..a1d28335 100644 --- a/lib/builtin/tickets/src/tickets_view.dart +++ b/lib/builtin/tickets/src/tickets_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:clide/builtin/tickets/src/ticket_colors.dart'; import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; import 'package:flutter/widgets.dart'; @@ -81,15 +82,17 @@ class _TicketsViewState extends State { } class _TicketEntry { - const _TicketEntry({required this.id, required this.title, this.status, this.priority}); + const _TicketEntry({required this.id, required this.title, this.type, this.status, this.priority}); final String id; final String title; + final String? type; final String? status; final String? priority; factory _TicketEntry.fromJson(Map json) => _TicketEntry( id: json['id'] as String? ?? '', title: json['title'] as String? ?? '', + type: json['type'] as String?, status: json['status'] as String?, priority: json['priority'] as String?, ); @@ -102,6 +105,9 @@ class _TicketRow extends StatelessWidget { @override Widget build(BuildContext context) { + final isDark = ClideTheme.of(context).dark; + final typeColors = TicketTypeColors.forTheme(dark: isDark); + final typeColor = typeColors.forType(entry.type); final statusColor = switch (entry.status) { 'done' => tokens.statusSuccess, 'in_progress' => tokens.statusInfo, @@ -114,7 +120,7 @@ class _TicketRow extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ - ClideText(entry.id, color: tokens.globalTextMuted, fontSize: 12), + ClideText(entry.id, color: typeColor, fontSize: 12, fontFamily: clideMonoFamily), const SizedBox(width: 6), Container( width: 6,