fix(state): changing species clears language, citizenship, and subspecies selections

This commit is contained in:
lew 2026-03-24 02:33:35 +00:00
parent 48f8b46827
commit c220815b89
20 changed files with 172 additions and 104 deletions

View file

@ -43,6 +43,7 @@
<xs:simpleType name="fieldType"> <xs:simpleType name="fieldType">
<xs:restriction base="xs:string"> <xs:restriction base="xs:string">
<xs:enumeration value="name" />
<xs:enumeration value="text" /> <xs:enumeration value="text" />
<xs:enumeration value="textarea" /> <xs:enumeration value="textarea" />
<xs:enumeration value="list" /> <xs:enumeration value="list" />
@ -57,6 +58,7 @@
<xs:enumeration value="subspecies" /> <xs:enumeration value="subspecies" />
<xs:enumeration value="citizenship" /> <xs:enumeration value="citizenship" />
<xs:enumeration value="languages" /> <xs:enumeration value="languages" />
<xs:enumeration value="separator" />
</xs:restriction> </xs:restriction>
</xs:simpleType> </xs:simpleType>

View file

@ -4,18 +4,8 @@
<record type="public"> <record type="public">
<note>Basic identification information visible on all records.</note> <note>Basic identification information visible on all records.</note>
<field label="Designation" type="text" /> <field label="Designation" type="name" />
<field label="Positronic Manufacture Date" type="date" placeholder="24-03-2460" />
<field label="Chassis Manufacture Date" type="date" placeholder="24-03-2460" />
<field label="IPC Model" type="subspecies" /> <field label="IPC Model" type="subspecies" />
<field label="Tag" type="text" />
<field label="Ownership Status" type="select">
<option value="company-owned" label="Company Owned" />
<option value="owned" label="Private Owner" />
<option value="free" label="Free" />
</field>
<field label="Owner Name" type="text" />
<field label="Owner Contact Information" type="text" />
<field label="Pronouns" type="multi-select"> <field label="Pronouns" type="multi-select">
<option value="he/him" label="he/him" /> <option value="he/him" label="he/him" />
<option value="she/her" label="she/her" /> <option value="she/her" label="she/her" />
@ -25,9 +15,21 @@
</field> </field>
<field label="Citizenship" type="citizenship" /> <field label="Citizenship" type="citizenship" />
<field label="Spoken Languages" type="languages" /> <field label="Spoken Languages" type="languages" />
<field label="Separator" type="separator" label="Chassis" />
<field label="Positronic Manufacture Date" type="date" placeholder="24-03-2460" />
<field label="Chassis Manufacture Date" type="date" placeholder="24-03-2460" />
<field label="Tag" type="text" />
<field label="Height" type="height" /> <field label="Height" type="height" />
<field label="Weight" type="weight" /> <field label="Weight" type="weight" />
<field label="Distinguishing Features" type="textarea" /> <field label="Distinguishing Features" type="textarea" />
<field label="Separator" type="separator" label="Ownership" />
<field label="Ownership Status" type="select">
<option value="company-owned" label="Company Owned" />
<option value="owned" label="Private Owner" />
<option value="free" label="Free" />
</field>
<field label="Owner Name" type="text" />
<field label="Owner Contact Information" type="text" />
</record> </record>
<record type="employment"> <record type="employment">

View file

@ -4,7 +4,7 @@
<record type="public"> <record type="public">
<note>Basic identification information visible on all records.</note> <note>Basic identification information visible on all records.</note>
<field label="Name" type="text" /> <field label="Name" type="name" />
<field label="Species" type="species" /> <field label="Species" type="species" />
<field label="Subspecies" type="subspecies" /> <field label="Subspecies" type="subspecies" />
<field label="Pronouns" type="multi-select"> <field label="Pronouns" type="multi-select">
@ -19,6 +19,7 @@
<field label="Spoken Languages" type="languages" /> <field label="Spoken Languages" type="languages" />
<field label="Employed As" type="text" /> <field label="Employed As" type="text" />
<field label="Next of Kin" type="text" /> <field label="Next of Kin" type="text" />
<field label="Separator" type="separator" label="Appearance" />
<field label="Height" type="height" /> <field label="Height" type="height" />
<field label="Weight" type="weight" /> <field label="Weight" type="weight" />
<field label="Skin Color" type="text" /> <field label="Skin Color" type="text" />

63
package-lock.json generated
View file

@ -828,9 +828,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -845,9 +842,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -862,9 +856,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -879,9 +870,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -896,9 +884,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -913,9 +898,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -930,9 +912,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -947,9 +926,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -964,9 +940,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -981,9 +954,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -998,9 +968,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -1015,9 +982,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -1032,9 +996,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -1375,9 +1336,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -1395,9 +1353,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -1415,9 +1370,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -1435,9 +1387,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -2285,9 +2234,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -2309,9 +2255,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -2333,9 +2276,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@ -2357,9 +2297,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [

View file

@ -2,8 +2,10 @@
import { roster } from '$lib/state.svelte'; import { roster } from '$lib/state.svelte';
import { slugify } from '$lib/utils/slugify'; import { slugify } from '$lib/utils/slugify';
function displayName(char: { data: Record<string, unknown> }): string { function displayName(char: { template: { records: { fields: { type: string; label: string }[] }[] }; data: Record<string, unknown> }): string {
const name = char.data[slugify('Name')]; const nameField = char.template.records.flatMap((r) => r.fields).find((f) => f.type === 'name');
const key = nameField ? slugify(nameField.label) : slugify('Name');
const name = char.data[key];
return (name as string) || 'Unnamed Character'; return (name as string) || 'Unnamed Character';
} }
</script> </script>

View file

@ -46,7 +46,9 @@
function displayName(): string { function displayName(): string {
const char = roster.active; const char = roster.active;
if (!char) return ''; if (!char) return '';
const name = char.data[slugify('Name')]; const nameField = char.template.records.flatMap((r) => r.fields).find((f) => f.type === 'name');
const key = nameField ? slugify(nameField.label) : slugify('Name');
const name = char.data[key];
return (name as string) || 'Unnamed Character'; return (name as string) || 'Unnamed Character';
} }

View file

@ -63,7 +63,9 @@
function charName(): string { function charName(): string {
if (!charData) return 'Unknown'; if (!charData) return 'Unknown';
return (charData.data[slugify('Name')] as string) || 'Unnamed Character'; const nameField = charData.template.records.flatMap((r: any) => r.fields).find((f: any) => f.type === 'name');
const key = nameField ? slugify(nameField.label) : slugify('Name');
return (charData.data[key] as string) || 'Unnamed Character';
} }
async function importCharacter() { async function importCharacter() {

View file

@ -13,8 +13,9 @@
let expanded = $state(false); let expanded = $state(false);
let dataFields = $derived(record.fields.filter((f) => f.type !== 'separator'));
let filled = $derived( let filled = $derived(
record.fields.filter((f) => { dataFields.filter((f) => {
const v = data[slugify(f.label)]; const v = data[slugify(f.label)];
if (v === undefined || v === null || v === '' || v === 0) return false; if (v === undefined || v === null || v === '' || v === 0) return false;
if (Array.isArray(v) && v.length === 0) return false; if (Array.isArray(v) && v.length === 0) return false;
@ -47,7 +48,7 @@
{/if} {/if}
</div> </div>
<span class="text-sm tabular-nums" style="color: var(--text-muted);"> <span class="text-sm tabular-nums" style="color: var(--text-muted);">
{filled}/{record.fields.length} {filled}/{dataFields.length}
</span> </span>
</button> </button>

View file

@ -14,6 +14,17 @@
let showTemplateSwitcher = $state(false); let showTemplateSwitcher = $state(false);
let showMigrationModal = $state(false); let showMigrationModal = $state(false);
let speciesKeys = $derived(new Set(
character.template.records.flatMap((r) => r.fields)
.filter((f) => f.type === 'species')
.map((f) => slugify(f.label))
));
const SPECIES_DEPENDENT_TYPES = new Set(['subspecies', 'citizenship', 'languages']);
let speciesDependentKeys = $derived(new Set(
character.template.records.flatMap((r) => r.fields)
.filter((f) => SPECIES_DEPENDENT_TYPES.has(f.type))
.map((f) => slugify(f.label))
));
let speciesKey = slugify('Species'); let speciesKey = slugify('Species');
let pendingMigration = $derived.by(() => { let pendingMigration = $derived.by(() => {
@ -59,9 +70,8 @@
return null; return null;
}); });
function switchTemplate(template: Template) { async function switchTemplate(template: Template) {
character.template = $state.snapshot(template); await roster.migrateToPreset(character, template);
roster.scheduleSave(character);
dismissed = null; dismissed = null;
showTemplateSwitcher = false; showTemplateSwitcher = false;
} }
@ -143,6 +153,11 @@
data={character.data} data={character.data}
onFieldChange={(key, value) => { onFieldChange={(key, value) => {
character.data[key] = value; character.data[key] = value;
if (speciesKeys.has(key)) {
for (const depKey of speciesDependentKeys) {
character.data[depKey] = '';
}
}
roster.scheduleSave(character); roster.scheduleSave(character);
}} }}
/> />

View file

@ -1,8 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { CitizenshipField } from '$lib/types'; import type { CitizenshipField } from '$lib/types';
import { citizenships } from '$lib/data'; import { citizenships, species } from '$lib/data';
import { slugify } from '$lib/utils/slugify';
let { field, value = '', onChange }: { field: CitizenshipField; value: string; onChange: (v: string) => void } = $props(); let { field, value = '', onChange, data }: {
field: CitizenshipField;
value: string;
onChange: (v: string) => void;
data: Record<string, unknown>;
} = $props();
let currentSpecies = $derived(species.find((s) => s.id === data[slugify('Species')]));
let filtered = $derived(
currentSpecies
? citizenships.filter((c) => currentSpecies!.citizenships.includes(c.id))
: citizenships
);
let custom = $state(false); let custom = $state(false);
@ -16,7 +29,7 @@
} }
} }
let isCustom = $derived(custom || (value !== '' && !citizenships.some((c) => c.name === value))); let isCustom = $derived(custom || (value !== '' && !filtered.some((c) => c.name === value)));
</script> </script>
<label class="block"> <label class="block">
@ -47,7 +60,7 @@
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);" style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
> >
<option value=""></option> <option value=""></option>
{#each citizenships as c} {#each filtered as c}
<option value={c.name}>{c.name}</option> <option value={c.name}>{c.name}</option>
{/each} {/each}
<option value="__custom">Other...</option> <option value="__custom">Other...</option>

View file

@ -14,6 +14,7 @@
import SubspeciesField from './SubspeciesField.svelte'; import SubspeciesField from './SubspeciesField.svelte';
import CitizenshipField from './CitizenshipField.svelte'; import CitizenshipField from './CitizenshipField.svelte';
import LanguagesField from './LanguagesField.svelte'; import LanguagesField from './LanguagesField.svelte';
import SeparatorField from './SeparatorField.svelte';
let { field, value, onChange, data }: { let { field, value, onChange, data }: {
field: FieldDef; field: FieldDef;
@ -23,7 +24,9 @@
} = $props(); } = $props();
</script> </script>
{#if field.type === 'text'} {#if field.type === 'name'}
<TextField field={{ ...field, type: 'text' }} {value} {onChange} />
{:else if field.type === 'text'}
<TextField {field} {value} {onChange} /> <TextField {field} {value} {onChange} />
{:else if field.type === 'textarea'} {:else if field.type === 'textarea'}
<TextareaField {field} {value} {onChange} /> <TextareaField {field} {value} {onChange} />
@ -48,7 +51,9 @@
{:else if field.type === 'subspecies'} {:else if field.type === 'subspecies'}
<SubspeciesField {field} {value} {onChange} {data} /> <SubspeciesField {field} {value} {onChange} {data} />
{:else if field.type === 'citizenship'} {:else if field.type === 'citizenship'}
<CitizenshipField {field} {value} {onChange} /> <CitizenshipField {field} {value} {onChange} {data} />
{:else if field.type === 'languages'} {:else if field.type === 'languages'}
<LanguagesField {field} {value} {onChange} /> <LanguagesField {field} {value} {onChange} {data} />
{:else if field.type === 'separator'}
<SeparatorField {field} />
{/if} {/if}

View file

@ -1,16 +1,24 @@
<script lang="ts"> <script lang="ts">
import type { LanguagesField } from '$lib/types'; import type { LanguagesField } from '$lib/types';
import { languages } from '$lib/data'; import { languages, species } from '$lib/data';
import { slugify } from '$lib/utils/slugify';
import MultiSelectField from './MultiSelectField.svelte'; import MultiSelectField from './MultiSelectField.svelte';
let { field, value = ['Tau Ceti Basic'], onChange }: { let { field, value = [], onChange, data }: {
field: LanguagesField; field: LanguagesField;
value: string[]; value: string[];
onChange: (v: string[]) => void; onChange: (v: string[]) => void;
data: Record<string, unknown>;
} = $props(); } = $props();
const options = languages.map((l) => ({ value: l.name, label: l.name })); let currentSpecies = $derived(species.find((s) => s.id === data[slugify('Species')]));
const multiField = $derived({ ...field, type: 'multi-select' as const, options }); let filtered = $derived(
currentSpecies
? languages.filter((l) => currentSpecies!.languages.includes(l.id))
: languages
);
let options = $derived(filtered.map((l) => ({ value: l.name, label: l.name })));
let multiField = $derived({ ...field, type: 'multi-select' as const, options });
</script> </script>
<MultiSelectField field={multiField} {value} {onChange} /> <MultiSelectField field={multiField} {value} {onChange} />

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { SeparatorField } from '$lib/types';
let { field }: { field: SeparatorField } = $props();
</script>
{#if field.label}
<div class="flex items-center gap-3 pt-2">
<hr class="flex-1" style="border-color: var(--border);" />
<span class="text-xs font-medium uppercase tracking-wide" style="color: var(--text-muted);">{field.label}</span>
<hr class="flex-1" style="border-color: var(--border);" />
</div>
{:else}
<hr class="mt-2" style="border-color: var(--border);" />
{/if}

View file

@ -68,6 +68,7 @@ function parseField(raw: any): FieldDef {
const type = raw['@_type']; const type = raw['@_type'];
switch (type) { switch (type) {
case 'name':
case 'text': case 'text':
case 'textarea': case 'textarea':
case 'date': case 'date':
@ -80,6 +81,8 @@ function parseField(raw: any): FieldDef {
case 'citizenship': case 'citizenship':
case 'languages': case 'languages':
return { ...base, type }; return { ...base, type };
case 'separator':
return { type: 'separator', label: raw['@_label'] ?? '' };
case 'number': case 'number':
return { return {
...base, ...base,

View file

@ -1,6 +1,7 @@
import type { Character, Template } from './types'; import type { Character, Template } from './types';
import { pruneEmpty } from './sharing'; import { pruneEmpty } from './sharing';
import { presets } from './presets'; import { presets } from './presets';
import { slugify } from './utils/slugify';
interface CharacterFilePayload { interface CharacterFilePayload {
version: number; version: number;
@ -44,7 +45,9 @@ export function parseCharacterFile(json: string): { template: Template | Omit<Te
} }
export function characterFileName(char: Character): string { export function characterFileName(char: Character): string {
const name = char.data.name as string | undefined; const nameField = char.template.records.flatMap((r) => r.fields).find((f) => f.type === 'name');
const key = nameField ? slugify(nameField.label) : 'name';
const name = char.data[key] as string | undefined;
if (!name || !name.trim()) return 'character.json'; if (!name || !name.trim()) return 'character.json';
return name.trim().replace(/[^a-zA-Z0-9'-]/g, '-').replace(/-+/g, '-') + '.json'; return name.trim().replace(/[^a-zA-Z0-9'-]/g, '-').replace(/-+/g, '-') + '.json';
} }

View file

@ -11,6 +11,7 @@ export function formatFieldOutput(
currentSpecies?: string currentSpecies?: string
): string | null { ): string | null {
switch (field.type) { switch (field.type) {
case 'name':
case 'text': case 'text':
case 'date': case 'date':
case 'citizenship': case 'citizenship':
@ -125,12 +126,32 @@ function renderFields(
speciesData?: SpeciesData[], speciesData?: SpeciesData[],
currentSpecies?: string currentSpecies?: string
): string[] { ): string[] {
const lines: string[] = []; // Split fields into groups by separator boundaries
const groups: FieldDef[][] = [[]];
for (const field of fields) { for (const field of fields) {
if (field.type === 'separator') {
groups.push([]);
} else {
groups[groups.length - 1].push(field);
}
}
const rendered = groups.map((group) => {
const lines: string[] = [];
for (const field of group) {
const out = formatFieldOutput(field, data[slugify(field.label)], speciesData, currentSpecies); const out = formatFieldOutput(field, data[slugify(field.label)], speciesData, currentSpecies);
if (out) lines.push(out); if (out) lines.push(out);
} }
return lines; return lines;
});
const result: string[] = [];
for (const lines of rendered) {
if (!lines.length) continue;
if (result.length) result.push('');
result.push(...lines);
}
return result;
} }
function splitLines(text: string | undefined): string[] { function splitLines(text: string | undefined): string[] {

View file

@ -4,6 +4,7 @@ import { slugify } from './utils/slugify';
function zodForField(field: FieldDef): z.ZodTypeAny { function zodForField(field: FieldDef): z.ZodTypeAny {
switch (field.type) { switch (field.type) {
case 'name':
case 'text': case 'text':
case 'textarea': case 'textarea':
case 'list': case 'list':
@ -30,6 +31,7 @@ export function buildCharacterSchema(template: Template): z.ZodObject<Record<str
const shape: Record<string, z.ZodTypeAny> = {}; const shape: Record<string, z.ZodTypeAny> = {};
for (const record of template.records) { for (const record of template.records) {
for (const field of record.fields) { for (const field of record.fields) {
if (field.type === 'separator') continue;
shape[slugify(field.label)] = zodForField(field); shape[slugify(field.label)] = zodForField(field);
} }
} }

View file

@ -9,6 +9,14 @@ let saveStatus = $state<'idle' | 'saving' | 'saved'>('idle');
let saveTimer: ReturnType<typeof setTimeout> | null = null; let saveTimer: ReturnType<typeof setTimeout> | null = null;
let statusTimer: ReturnType<typeof setTimeout> | null = null; let statusTimer: ReturnType<typeof setTimeout> | null = null;
const SINGLETON_TYPES = new Set([
'name', 'species', 'subspecies', 'citizenship', 'languages', 'height', 'weight'
]);
function allFields(template: Template) {
return template.records.flatMap((r) => r.fields).filter((f) => f.type !== 'separator');
}
function migrateData(char: Character, preset: Template) { function migrateData(char: Character, preset: Template) {
for (const record of preset.records) { for (const record of preset.records) {
for (const field of record.fields) { for (const field of record.fields) {
@ -26,6 +34,22 @@ function migrateData(char: Character, preset: Template) {
} }
} }
} }
const oldByType = new Map<string, string>();
for (const f of allFields(char.template)) {
if (SINGLETON_TYPES.has(f.type)) {
oldByType.set(f.type, slugify(f.label));
}
}
for (const f of allFields(preset)) {
if (!SINGLETON_TYPES.has(f.type)) continue;
const newKey = slugify(f.label);
if (char.data[newKey] !== undefined) continue;
const oldKey = oldByType.get(f.type);
if (oldKey && oldKey !== newKey && char.data[oldKey] !== undefined) {
char.data[newKey] = char.data[oldKey];
}
}
} }
export const roster = { export const roster = {

View file

@ -9,6 +9,11 @@ export interface BaseFieldDef {
from?: string; from?: string;
} }
export interface NameField extends BaseFieldDef {
type: 'name';
placeholder?: string;
}
export interface TextField extends BaseFieldDef { export interface TextField extends BaseFieldDef {
type: 'text'; type: 'text';
placeholder?: string; placeholder?: string;
@ -74,7 +79,13 @@ export interface LanguagesField extends BaseFieldDef {
type: 'languages'; type: 'languages';
} }
export interface SeparatorField {
type: 'separator';
label: string;
}
export type FieldDef = export type FieldDef =
| NameField
| TextField | TextField
| TextareaField | TextareaField
| ListField | ListField
@ -88,7 +99,8 @@ export type FieldDef =
| SpeciesField | SpeciesField
| SubspeciesField | SubspeciesField
| CitizenshipField | CitizenshipField
| LanguagesField; | LanguagesField
| SeparatorField;
export interface RecordDef { export interface RecordDef {
type: string; type: string;

View file

@ -1,13 +1,11 @@
import type { Character } from '../types'; import type { Character } from '../types';
const DEFAULT_LANGUAGES = ['Tau Ceti Basic'];
export function isBlankCharacter(char: Character): boolean { export function isBlankCharacter(char: Character): boolean {
if (!char.data) return true;
for (const value of Object.values(char.data)) { for (const value of Object.values(char.data)) {
if (value === '' || value === undefined || value === null || value === 0) continue; if (value === '' || value === undefined || value === null || value === 0) continue;
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length === 0) continue; if (value.length === 0) continue;
if (value.length === 1 && DEFAULT_LANGUAGES.includes(value[0])) continue;
return false; return false;
} }
return false; return false;