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:
@@ -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
|
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||||
hat from sidebar from status bar if desired.
|
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
|
## Anti-patterns
|
||||||
|
|
||||||
- Borrowing another surface's token (`sidebarBackground` for hat bar)
|
- 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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:clide/builtin/tickets/src/ticket_colors.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@@ -81,15 +82,17 @@ class _TicketsViewState extends State<TicketsView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TicketEntry {
|
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 id;
|
||||||
final String title;
|
final String title;
|
||||||
|
final String? type;
|
||||||
final String? status;
|
final String? status;
|
||||||
final String? priority;
|
final String? priority;
|
||||||
|
|
||||||
factory _TicketEntry.fromJson(Map<String, dynamic> json) => _TicketEntry(
|
factory _TicketEntry.fromJson(Map<String, dynamic> json) => _TicketEntry(
|
||||||
id: json['id'] as String? ?? '',
|
id: json['id'] as String? ?? '',
|
||||||
title: json['title'] as String? ?? '',
|
title: json['title'] as String? ?? '',
|
||||||
|
type: json['type'] as String?,
|
||||||
status: json['status'] as String?,
|
status: json['status'] as String?,
|
||||||
priority: json['priority'] as String?,
|
priority: json['priority'] as String?,
|
||||||
);
|
);
|
||||||
@@ -102,6 +105,9 @@ class _TicketRow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
final statusColor = switch (entry.status) {
|
||||||
'done' => tokens.statusSuccess,
|
'done' => tokens.statusSuccess,
|
||||||
'in_progress' => tokens.statusInfo,
|
'in_progress' => tokens.statusInfo,
|
||||||
@@ -114,7 +120,7 @@ class _TicketRow extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
ClideText(entry.id, color: tokens.globalTextMuted, fontSize: 12),
|
ClideText(entry.id, color: typeColor, fontSize: 12, fontFamily: clideMonoFamily),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Container(
|
Container(
|
||||||
width: 6,
|
width: 6,
|
||||||
|
|||||||
Reference in New Issue
Block a user