fix(state): changing species clears language, citizenship, and subspecies selections
This commit is contained in:
parent
48f8b46827
commit
c220815b89
20 changed files with 172 additions and 104 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
63
package-lock.json
generated
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
15
src/lib/components/fields/SeparatorField.svelte
Normal file
15
src/lib/components/fields/SeparatorField.svelte
Normal 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}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
const out = formatFieldOutput(field, data[slugify(field.label)], speciesData, currentSpecies);
|
if (field.type === 'separator') {
|
||||||
if (out) lines.push(out);
|
groups.push([]);
|
||||||
|
} else {
|
||||||
|
groups[groups.length - 1].push(field);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return lines;
|
|
||||||
|
const rendered = groups.map((group) => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const field of group) {
|
||||||
|
const out = formatFieldOutput(field, data[slugify(field.label)], speciesData, currentSpecies);
|
||||||
|
if (out) lines.push(out);
|
||||||
|
}
|
||||||
|
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[] {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue