Files
task-stream/libs/web/tracker/presenters/src/lib/ui/copy.ts
2026-10-03 13:40:26 +02:00

244 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DatasetColor } from '@pi/ui';
import {
IssueArea,
IssueLinkType,
IssuePriority,
IssueStatus,
IssueType,
ProjectRole,
ReleaseStatus,
} from '@task-stream/web/tracker/domain';
type ChipColor =
| 'accent'
| 'grey'
| 'highlighted'
| 'primary'
| 'success'
| 'warn';
type IconColor =
| 'primary'
| 'accent'
| 'warn'
| 'success'
| 'grey'
| 'highlighted';
/* UI copy (es) and the visual language of the tracker. */
/** `tone`: CSS colour token of the status/type (see `--ts-s-*`, `--ts-t-*` in styles.scss). */
export const STATUS: Record<
IssueStatus,
{ label: string; color: ChipColor; chart: DatasetColor; tone: string }
> = {
[IssueStatus.BACKLOG]: {
label: 'Backlog',
color: 'grey',
chart: DatasetColor.grey,
tone: 'var(--ts-s-backlog)',
},
[IssueStatus.TODO]: {
label: 'Por hacer',
color: 'grey',
chart: DatasetColor.grey300,
tone: 'var(--ts-s-todo)',
},
[IssueStatus.IN_PROGRESS]: {
label: 'En curso',
color: 'primary',
chart: DatasetColor.primary,
tone: 'var(--ts-s-progress)',
},
[IssueStatus.IN_REVIEW]: {
label: 'En revisión',
color: 'accent',
chart: DatasetColor.accent,
tone: 'var(--ts-s-review)',
},
[IssueStatus.READY_FOR_QA]: {
label: 'Listo para QA',
color: 'highlighted',
chart: DatasetColor.highlighted,
tone: 'var(--ts-s-ready)',
},
[IssueStatus.IN_QA]: {
label: 'En QA',
color: 'highlighted',
chart: DatasetColor.highlighted50,
tone: 'var(--ts-s-qa)',
},
[IssueStatus.QA_FAILED]: {
label: 'QA fallido',
color: 'warn',
chart: DatasetColor.warn,
tone: 'var(--ts-s-failed)',
},
[IssueStatus.DONE]: {
label: 'Hecho',
color: 'success',
chart: DatasetColor.success,
tone: 'var(--ts-s-done)',
},
[IssueStatus.CANCELLED]: {
label: 'Cancelado',
color: 'grey',
chart: DatasetColor.grey50,
tone: 'var(--ts-s-cancelled)',
},
};
export const TYPE: Record<
IssueType,
{ label: string; icon: string; color: IconColor; tone: string }
> = {
[IssueType.BUG]: {
label: 'Bug',
icon: 'bug',
color: 'warn',
tone: 'var(--ts-t-bug)',
},
[IssueType.FEATURE]: {
label: 'Feature',
icon: 'feature',
color: 'primary',
tone: 'var(--ts-t-feature)',
},
[IssueType.UI_CHANGE]: {
label: 'Cambio de UI',
icon: 'palette',
color: 'accent',
tone: 'var(--ts-t-ui)',
},
[IssueType.TASK]: {
label: 'Tarea',
icon: 'task',
color: 'grey',
tone: 'var(--ts-t-task)',
},
[IssueType.QA]: {
label: 'QA',
icon: 'qa',
color: 'highlighted',
tone: 'var(--ts-t-qa)',
},
};
/** Signal strength of a priority, drawn as bars (1–4) like a phone signal. */
export const PRIORITY_LEVEL: Record<IssuePriority, number> = {
[IssuePriority.CRITICAL]: 4,
[IssuePriority.HIGH]: 3,
[IssuePriority.MEDIUM]: 2,
[IssuePriority.LOW]: 1,
[IssuePriority.LOWEST]: 0,
};
export const PRIORITY: Record<
IssuePriority,
{ label: string; color: ChipColor }
> = {
[IssuePriority.CRITICAL]: { label: 'Crítica', color: 'warn' },
[IssuePriority.HIGH]: { label: 'Alta', color: 'accent' },
[IssuePriority.MEDIUM]: { label: 'Media', color: 'primary' },
[IssuePriority.LOW]: { label: 'Baja', color: 'grey' },
[IssuePriority.LOWEST]: { label: 'Mínima', color: 'grey' },
};
export const AREA: Record<IssueArea, string> = {
[IssueArea.FRONTEND]: 'Frontend',
[IssueArea.BACKEND]: 'Backend',
[IssueArea.MOBILE]: 'Mobile',
[IssueArea.QA]: 'QA',
[IssueArea.DESIGN]: 'Diseño',
[IssueArea.DEVOPS]: 'DevOps',
};
export const LINK: Record<IssueLinkType, { outward: string; inward: string }> =
{
[IssueLinkType.BLOCKS]: {
outward: 'bloquea a',
inward: 'está bloqueado por',
},
[IssueLinkType.DEPENDS_ON]: {
outward: 'depende de',
inward: 'es dependencia de',
},
[IssueLinkType.TESTS]: { outward: 'prueba', inward: 'es probado por' },
[IssueLinkType.DUPLICATES]: {
outward: 'duplica a',
inward: 'está duplicado por',
},
[IssueLinkType.RELATES_TO]: {
outward: 'relacionado con',
inward: 'relacionado con',
},
};
export const ROLE: Record<ProjectRole, string> = {
[ProjectRole.OWNER]: 'Propietario',
[ProjectRole.ADMIN]: 'Admin',
[ProjectRole.MEMBER]: 'Miembro',
[ProjectRole.VIEWER]: 'Lector',
};
export const RELEASE_STATUS: Record<
ReleaseStatus,
{ label: string; color: ChipColor }
> = {
[ReleaseStatus.PLANNED]: { label: 'Planificada', color: 'primary' },
[ReleaseStatus.RELEASED]: { label: 'Publicada', color: 'success' },
[ReleaseStatus.ARCHIVED]: { label: 'Archivada', color: 'grey' },
};
export const options = <T extends string>(
record: Record<T, string | { label: string }>,
) =>
(Object.keys(record) as T[]).map((value) => {
const entry = record[value];
return { value, label: typeof entry === 'string' ? entry : entry.label };
});
/** "hace 5 min", "ayer"… */
export function timeAgo(iso: string, now = Date.now()): string {
const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1000));
const rtf = new Intl.RelativeTimeFormat('es', { numeric: 'auto' });
if (seconds < 60) return rtf.format(-seconds, 'second');
if (seconds < 3600) return rtf.format(-Math.round(seconds / 60), 'minute');
if (seconds < 86400) return rtf.format(-Math.round(seconds / 3600), 'hour');
return rtf.format(-Math.round(seconds / 86400), 'day');
}
const PROJECT_TONES = [
'var(--ts-t-feature)',
'var(--ts-s-review)',
'var(--ts-t-ui)',
'var(--ts-t-qa)',
'var(--ts-s-done)',
'var(--ts-s-qa)',
];
/** A stable colour per project (from its key), so it is recognisable everywhere. */
export function projectTone(key: string): string {
let hash = 0;
for (const char of key) hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
return PROJECT_TONES[hash % PROJECT_TONES.length];
}
/** "Ana García" → "AG". */
export function initials(name: string): string {
return name
.split(/\s+/)
.filter(Boolean)
.map((part) => part[0])
.join('')
.slice(0, 2)
.toUpperCase();
}
/** "14:05" today, "3 oct" otherwise (logs and histories). */
export function clock(iso: string, now = new Date()): string {
const date = new Date(iso);
return date.toDateString() === now.toDateString()
? date.toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' })
: date.toLocaleDateString('es', { day: 'numeric', month: 'short' });
}