feat: statuses and ticket types take any colour from a colour picker

Settings shows a native colour picker (the palette as suggestions)
instead of the list of eleven names. The API takes a palette name, as
before, or any #rrggbb; picking a palette colour still saves its name,
so it keeps its hand-made dark version. A free colour gets one variant
per theme, computed with the `color` library (moved towards white or
black until it contrasts 3:1 with the surface) and applied with
light-dark(): a pale yellow darkens on white, a navy lightens on dark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 16:08:47 +02:00
parent 034704e46f
commit 33c593521e
19 changed files with 379 additions and 115 deletions

View File

@@ -661,7 +661,9 @@ export class WorkflowStatusInput {
@Field(() => StatusCategory)
@IsEnum(StatusCategory)
category!: StatusCategory;
@Field() @IsString() color!: string;
@Field({ description: 'A palette colour (follows the theme) or #rrggbb' })
@IsString()
color!: string;
@Field({ defaultValue: false }) @IsBoolean() requiresReason!: boolean;
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
@Field(() => Int, {
@@ -705,7 +707,9 @@ export class IssueTypeOptionInput {
@Field() @Matches(SETTING_ID) id!: string;
@Field() @IsString() @MinLength(1) @MaxLength(40) name!: string;
@Field() @IsString() icon!: string;
@Field() @IsString() color!: string;
@Field({ description: 'A palette colour (follows the theme) or #rrggbb' })
@IsString()
color!: string;
@Field({ defaultValue: false }) @IsBoolean() archived!: boolean;
@Field(() => IssueTemplateInput, { nullable: true })
@IsOptional()

View File

@@ -35,7 +35,11 @@ export class WorkflowStatusGql {
id!: string;
@Field() name!: string;
@Field(() => StatusCategory) category!: StatusCategory;
@Field({ description: 'Colour token: slate, violet, green…' }) color!: string;
@Field({
description:
'A palette colour (slate, violet, green…, follows the theme) or #rrggbb',
})
color!: string;
@Field({ description: 'Entering it needs a reason' })
requiresReason!: boolean;
@Field({ description: 'No new tickets or transitions into it' })

View File

@@ -10,7 +10,7 @@ import {
IssuePriority,
ProjectRole,
REACTIONS,
SETTING_COLORS,
SETTING_COLOR_PATTERN,
StatusCategory,
TYPE_ICONS,
} from '@task-stream/api/tracker/domain';
@@ -153,8 +153,15 @@ export class WorkflowStatusBodyDto {
@ApiProperty({ enum: StatusCategory, enumName: 'StatusCategory' })
@IsEnum(StatusCategory)
category!: StatusCategory;
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
@IsIn(SETTING_COLORS)
@ApiProperty({
description:
'A palette colour (slate, stone, violet, blue, sky, teal, green, amber, orange, red, pink), which follows the theme, or any #rrggbb',
example: '#7c3aed',
pattern: SETTING_COLOR_PATTERN.source,
})
@Matches(SETTING_COLOR_PATTERN, {
message: 'color must be a palette colour or #rrggbb',
})
color!: string;
@ApiPropertyOptional({ type: Boolean, default: false })
@IsOptional()
@@ -212,8 +219,15 @@ export class IssueTypeOptionBodyDto {
@ApiProperty({ enum: TYPE_ICONS, enumName: 'TypeIcon' })
@IsIn(TYPE_ICONS)
icon!: string;
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
@IsIn(SETTING_COLORS)
@ApiProperty({
description:
'A palette colour (slate, stone, violet, blue, sky, teal, green, amber, orange, red, pink), which follows the theme, or any #rrggbb',
example: '#7c3aed',
pattern: SETTING_COLOR_PATTERN.source,
})
@Matches(SETTING_COLOR_PATTERN, {
message: 'color must be a palette colour or #rrggbb',
})
color!: string;
@ApiPropertyOptional({ type: Boolean, default: false })
@IsOptional()

View File

@@ -7,7 +7,6 @@ import {
ProjectRole,
ReleaseStatus,
SprintStatus,
SETTING_COLORS,
StatusCategory,
TYPE_ICONS,
} from '@task-stream/api/tracker/domain';
@@ -34,7 +33,10 @@ export class WorkflowStatusDto {
@ApiProperty({ example: 'En revisión' }) name!: string;
@ApiProperty({ enum: StatusCategory, enumName: 'StatusCategory' })
category!: StatusCategory;
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
@ApiProperty({
description: 'A palette colour (follows the theme) or #rrggbb',
example: 'violet',
})
color!: string;
@ApiProperty({ description: 'Entering it needs a reason' })
requiresReason!: boolean;
@@ -63,7 +65,10 @@ export class IssueTypeOptionDto {
@ApiProperty({ example: 'BUG' }) id!: string;
@ApiProperty({ example: 'Bug' }) name!: string;
@ApiProperty({ enum: TYPE_ICONS, enumName: 'TypeIcon' }) icon!: string;
@ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' })
@ApiProperty({
description: 'A palette colour (follows the theme) or #rrggbb',
example: 'violet',
})
color!: string;
@ApiProperty() archived!: boolean;
@ApiPropertyOptional({