mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 06:28:13 +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.
|
* Badge colours for licence types.
|
||||||
*
|
*
|
||||||
* Red and yellow are left out on purpose: they read as a problem, and a
|
* Red is left out: it reads as a problem, and a licence type is not one.
|
||||||
* licence type is not one. Green is out too — it means verified elsewhere in
|
* Everything else the theme offers is in, because the point of the colour is
|
||||||
* the backoffice.
|
* telling two licence types apart at a glance.
|
||||||
*/
|
*/
|
||||||
const SCOPE_COLORS = [
|
const SCOPE_COLORS = [
|
||||||
'grape',
|
|
||||||
'violet',
|
|
||||||
'indigo',
|
|
||||||
'cyan',
|
|
||||||
'teal',
|
|
||||||
'pink',
|
|
||||||
'orange',
|
|
||||||
'blue',
|
'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 —
|
* A colour per licence type, assigned by position in the catalogue.
|
||||||
* derived from its id rather than its position, so a colour is something the
|
*
|
||||||
* eye can learn instead of a lottery per render.
|
* 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 {
|
function buildScopeStyles(
|
||||||
let hash = 0;
|
types: { id: string; sortOrder: number }[],
|
||||||
for (let i = 0; i < licenseTypeId.length; i += 1) {
|
): Map<string, { color: string; variant: (typeof SCOPE_VARIANTS)[number] }> {
|
||||||
hash = (hash * 31 + licenseTypeId.charCodeAt(i)) % 997;
|
const styles = new Map<
|
||||||
}
|
string,
|
||||||
return SCOPE_COLORS[hash % SCOPE_COLORS.length];
|
{ 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. */
|
/** `application/vnd.openxmlformats-…-document` is nobody's idea of a column. */
|
||||||
@@ -173,6 +203,11 @@ export function PersonalDocumentsCard() {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scopeStyles = useMemo(
|
||||||
|
() => buildScopeStyles(licenseTypes?.items ?? []),
|
||||||
|
[licenseTypes],
|
||||||
|
);
|
||||||
|
|
||||||
const typeName = (id: string) => {
|
const typeName = (id: string) => {
|
||||||
const found = (licenseTypes?.items ?? []).find((lt) => lt.id === id);
|
const found = (licenseTypes?.items ?? []).find((lt) => lt.id === id);
|
||||||
return found ? localized(found.name) || found.key : id;
|
return found ? localized(found.name) || found.key : id;
|
||||||
@@ -206,11 +241,15 @@ export function PersonalDocumentsCard() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
{row.original.scope.map((id) => (
|
{row.original.scope.map((id) => {
|
||||||
<Badge key={id} size="sm" variant="light" color={scopeColor(id)}>
|
// A type the catalogue no longer lists still needs a badge.
|
||||||
{typeName(id)}
|
const style = scopeStyles.get(id) ?? { color: 'gray', variant: 'light' };
|
||||||
</Badge>
|
return (
|
||||||
))}
|
<Badge key={id} size="sm" variant={style.variant} color={style.color}>
|
||||||
|
{typeName(id)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Group>
|
</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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[t, localized, licenseTypes],
|
[t, localized, licenseTypes, scopeStyles],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user