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) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 12:32:11 +02:00
co-authored by Claude Opus 4.6
parent 310f1a88c0
commit d52e1130ec
3 changed files with 66 additions and 2 deletions
+13
View File
@@ -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.<id>.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)
@@ -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;
}
+8 -2
View File
@@ -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<TicketsView> {
}
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<String, dynamic> 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,