feat: dynamic field labels, optionally overriding field names per species

This commit is contained in:
Lewis Wynne 2026-04-07 18:17:41 +01:00
parent a0060ca4bb
commit a2b904811a
21 changed files with 322 additions and 73 deletions

View file

@ -1,8 +1,10 @@
import type { Character } from '../types';
import { FLAGS_KEY } from './field-flags';
export function isBlankCharacter(char: Character): boolean {
if (!char.data) return true;
for (const value of Object.values(char.data)) {
for (const [key, value] of Object.entries(char.data)) {
if (key === FLAGS_KEY) continue;
if (value === '' || value === undefined || value === null || value === 0) continue;
if (Array.isArray(value)) {
if (value.length === 0) continue;

View file

@ -0,0 +1,44 @@
export const FLAGS_KEY = '__flags';
export interface FieldFlags {
useSpeciesLabel?: boolean;
hidden?: boolean;
}
const DEFAULTS: Required<FieldFlags> = {
useSpeciesLabel: true,
hidden: false
};
export function getFlag(
data: Record<string, unknown>,
fieldKey: string,
flag: keyof FieldFlags
): boolean {
const all = data[FLAGS_KEY] as Record<string, FieldFlags> | undefined;
return all?.[fieldKey]?.[flag] ?? DEFAULTS[flag];
}
export function setFlag(
data: Record<string, unknown>,
fieldKey: string,
flag: keyof FieldFlags,
value: boolean
): void {
if (!data[FLAGS_KEY]) data[FLAGS_KEY] = {};
const all = data[FLAGS_KEY] as Record<string, FieldFlags>;
if (!all[fieldKey]) all[fieldKey] = {};
if (value === DEFAULTS[flag]) {
delete all[fieldKey][flag];
if (Object.keys(all[fieldKey]).length === 0) delete all[fieldKey];
if (Object.keys(all).length === 0) delete data[FLAGS_KEY];
} else {
all[fieldKey][flag] = value;
}
}
export function getAllFlags(
data: Record<string, unknown>
): Record<string, FieldFlags> {
return (data[FLAGS_KEY] as Record<string, FieldFlags>) ?? {};
}

View file

@ -0,0 +1,37 @@
import { describe, it, expect } from 'vitest';
import { resolveFieldLabel } from './resolve-label';
import type { FieldDef } from '$lib/types';
import type { SpeciesData } from '$lib/data/types';
const stubSpecies: SpeciesData[] = [
{
id: 'tajara',
name: 'Tajara',
labels: { subspecies: 'Ethnicity', 'skin-color': 'Fur Colour' },
subspecies: [{ id: 'hharar', name: 'Hharar' }],
languages: [],
citizenships: []
}
];
describe('resolveFieldLabel', () => {
it('returns species override when available', () => {
const field: FieldDef = { label: 'Skin Color', type: 'text' };
expect(resolveFieldLabel(field, stubSpecies, 'tajara')).toBe('Fur Colour');
});
it('returns null when no species is selected', () => {
const field: FieldDef = { label: 'Skin Color', type: 'text' };
expect(resolveFieldLabel(field, stubSpecies, undefined)).toBeNull();
});
it('returns null when species has no label for the field', () => {
const field: FieldDef = { label: 'Height', type: 'height' };
expect(resolveFieldLabel(field, stubSpecies, 'tajara')).toBeNull();
});
it('returns null for separator fields', () => {
const field: FieldDef = { label: 'Appearance', type: 'separator' };
expect(resolveFieldLabel(field, stubSpecies, 'tajara')).toBeNull();
});
});

View file

@ -0,0 +1,17 @@
import type { FieldDef } from '$lib/types';
import type { SpeciesData } from '$lib/data/types';
import { slugify } from './slugify';
export function resolveFieldLabel(
field: FieldDef,
speciesData: SpeciesData[],
currentSpeciesId: string | undefined
): string | null {
if (field.type === 'separator') return null;
if (!currentSpeciesId) return null;
const sp = speciesData.find((s) => s.id === currentSpeciesId);
if (!sp) return null;
return sp.labels[slugify(field.label)] ?? null;
}