mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-28 20:40:56 +00:00
refactor: replace hash-based license badge coloring with a deterministic, collision-resistant palette based on sort order
This commit is contained in:
@@ -49,32 +49,62 @@ const SEARCH_DEBOUNCE_MS = 300;
|
||||
/**
|
||||
* Badge colours for licence types.
|
||||
*
|
||||
* Red and yellow are left out on purpose: they read as a problem, and a
|
||||
* licence type is not one. Green is out too — it means verified elsewhere in
|
||||
* the backoffice.
|
||||
* Red is left out: it reads as a problem, and a licence type is not one.
|
||||
* Everything else the theme offers is in, because the point of the colour is
|
||||
* telling two licence types apart at a glance.
|
||||
*/
|
||||
const SCOPE_COLORS = [
|
||||
'grape',
|
||||
'violet',
|
||||
'indigo',
|
||||
'cyan',
|
||||
'teal',
|
||||
'pink',
|
||||
'orange',
|
||||
'blue',
|
||||
'grape',
|
||||
'teal',
|
||||
'orange',
|
||||
'violet',
|
||||
'cyan',
|
||||
'pink',
|
||||
'lime',
|
||||
'indigo',
|
||||
'green',
|
||||
'yellow',
|
||||
'gray',
|
||||
];
|
||||
|
||||
/** Doubles the palette: the same hue, a visibly different badge. */
|
||||
const SCOPE_VARIANTS = ['light', 'outline'] as const;
|
||||
|
||||
/**
|
||||
* The same licence type keeps the same colour on every row and every visit —
|
||||
* derived from its id rather than its position, so a colour is something the
|
||||
* eye can learn instead of a lottery per render.
|
||||
* A colour per licence type, assigned by position in the catalogue.
|
||||
*
|
||||
* Hashing the id looked tidier and was wrong: eight buckets over sixteen
|
||||
* licence types collide by the pigeonhole principle, so Vessel Registration
|
||||
* and Freight Forwarder came out the same colour and the badge stopped
|
||||
* carrying information. Walking the sorted catalogue instead gives every type
|
||||
* a distinct colour until the palette runs out, and only then repeats a hue in
|
||||
* the other variant — 24 distinct badges before any two can look alike.
|
||||
*
|
||||
* Sorted by `sortOrder` so the assignment is the same for every officer and
|
||||
* survives a refresh; a type added later takes the next free style rather than
|
||||
* reshuffling the ones already learned.
|
||||
*/
|
||||
function scopeColor(licenseTypeId: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < licenseTypeId.length; i += 1) {
|
||||
hash = (hash * 31 + licenseTypeId.charCodeAt(i)) % 997;
|
||||
}
|
||||
return SCOPE_COLORS[hash % SCOPE_COLORS.length];
|
||||
function buildScopeStyles(
|
||||
types: { id: string; sortOrder: number }[],
|
||||
): Map<string, { color: string; variant: (typeof SCOPE_VARIANTS)[number] }> {
|
||||
const styles = new Map<
|
||||
string,
|
||||
{ color: string; variant: (typeof SCOPE_VARIANTS)[number] }
|
||||
>();
|
||||
types
|
||||
.slice()
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.forEach((type, index) => {
|
||||
styles.set(type.id, {
|
||||
color: SCOPE_COLORS[index % SCOPE_COLORS.length],
|
||||
variant:
|
||||
SCOPE_VARIANTS[
|
||||
Math.floor(index / SCOPE_COLORS.length) % SCOPE_VARIANTS.length
|
||||
],
|
||||
});
|
||||
});
|
||||
return styles;
|
||||
}
|
||||
|
||||
/** `application/vnd.openxmlformats-…-document` is nobody's idea of a column. */
|
||||
@@ -173,6 +203,11 @@ export function PersonalDocumentsCard() {
|
||||
setPageIndex(0);
|
||||
}
|
||||
|
||||
const scopeStyles = useMemo(
|
||||
() => buildScopeStyles(licenseTypes?.items ?? []),
|
||||
[licenseTypes],
|
||||
);
|
||||
|
||||
const typeName = (id: string) => {
|
||||
const found = (licenseTypes?.items ?? []).find((lt) => lt.id === id);
|
||||
return found ? localized(found.name) || found.key : id;
|
||||
@@ -206,11 +241,15 @@ export function PersonalDocumentsCard() {
|
||||
</Badge>
|
||||
) : (
|
||||
<Group gap={4}>
|
||||
{row.original.scope.map((id) => (
|
||||
<Badge key={id} size="sm" variant="light" color={scopeColor(id)}>
|
||||
{typeName(id)}
|
||||
</Badge>
|
||||
))}
|
||||
{row.original.scope.map((id) => {
|
||||
// A type the catalogue no longer lists still needs a badge.
|
||||
const style = scopeStyles.get(id) ?? { color: 'gray', variant: 'light' };
|
||||
return (
|
||||
<Badge key={id} size="sm" variant={style.variant} color={style.color}>
|
||||
{typeName(id)}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
@@ -274,9 +313,9 @@ export function PersonalDocumentsCard() {
|
||||
),
|
||||
},
|
||||
],
|
||||
// `typeName` closes over the licence-type list, which `localized` also reads.
|
||||
// `typeName` and `scopeStyles` both close over the licence-type list.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[t, localized, licenseTypes],
|
||||
[t, localized, licenseTypes, scopeStyles],
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user