feat(fend): adds record cards to each record section
This commit is contained in:
parent
973541e24f
commit
85b87f8140
3 changed files with 81 additions and 24 deletions
58
src/lib/components/RecordCard.svelte
Normal file
58
src/lib/components/RecordCard.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { ChevronDown } from 'lucide-svelte';
|
||||||
|
import type { RecordDef } from '$lib/types';
|
||||||
|
import DynamicField from './fields/DynamicField.svelte';
|
||||||
|
import { slugify } from '$lib/utils/slugify';
|
||||||
|
|
||||||
|
let { record, data, onFieldChange }: {
|
||||||
|
record: RecordDef;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
onFieldChange: (key: string, value: any) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let expanded = $state(false);
|
||||||
|
|
||||||
|
let filled = $derived(
|
||||||
|
record.fields.filter((f) => {
|
||||||
|
const v = data[slugify(f.label)];
|
||||||
|
if (v === undefined || v === null || v === '' || v === 0) return false;
|
||||||
|
if (Array.isArray(v) && v.length === 0) return false;
|
||||||
|
return true;
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="rounded border" style="border-color: var(--border); background: var(--bg-card);">
|
||||||
|
<button
|
||||||
|
onclick={() => { expanded = !expanded; }}
|
||||||
|
class="flex items-center w-full px-4 py-3 text-left gap-3"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
class="transition-transform shrink-0"
|
||||||
|
style="transform: rotate({expanded ? '0' : '-90'}deg); color: var(--text-muted);"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<span class="font-medium capitalize">{record.type}</span>
|
||||||
|
{#if record.preamble}
|
||||||
|
<span class="block text-xs truncate" style="color: var(--text-muted);">{record.preamble}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm tabular-nums" style="color: var(--text-muted);">
|
||||||
|
{filled}/{record.fields.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if expanded}
|
||||||
|
<div class="px-4 pb-4 flex flex-col gap-4">
|
||||||
|
{#each record.fields as field}
|
||||||
|
<DynamicField
|
||||||
|
{field}
|
||||||
|
value={data[slugify(field.label)]}
|
||||||
|
{data}
|
||||||
|
onChange={(v) => onFieldChange(slugify(field.label), v)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
20
src/lib/components/SchemaForm.svelte
Normal file
20
src/lib/components/SchemaForm.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Character } from '$lib/types';
|
||||||
|
import { roster } from '$lib/state.svelte';
|
||||||
|
import RecordCard from './RecordCard.svelte';
|
||||||
|
|
||||||
|
let { character }: { character: Character } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{#each character.template.records as record}
|
||||||
|
<RecordCard
|
||||||
|
{record}
|
||||||
|
data={character.data}
|
||||||
|
onFieldChange={(key, value) => {
|
||||||
|
character.data[key] = value;
|
||||||
|
roster.scheduleSave(character);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Header from '$lib/components/Header.svelte';
|
import Header from '$lib/components/Header.svelte';
|
||||||
import DynamicField from '$lib/components/fields/DynamicField.svelte';
|
import SchemaForm from '$lib/components/SchemaForm.svelte';
|
||||||
import { roster } from '$lib/state.svelte';
|
import { roster } from '$lib/state.svelte';
|
||||||
import { presets } from '$lib/presets';
|
import { presets } from '$lib/presets';
|
||||||
import { slugify } from '$lib/utils/slugify';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="min-h-screen flex flex-col">
|
||||||
|
|
@ -11,28 +10,8 @@
|
||||||
|
|
||||||
<main class="flex-1 p-4">
|
<main class="flex-1 p-4">
|
||||||
{#if roster.active}
|
{#if roster.active}
|
||||||
{@const char = roster.active}
|
<div class="max-w-xl mx-auto">
|
||||||
<div class="max-w-xl mx-auto flex flex-col gap-6">
|
<SchemaForm character={roster.active} />
|
||||||
{#each char.template.records as record}
|
|
||||||
<section>
|
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-wide mb-3" style="color: var(--text-muted);">
|
|
||||||
{record.type}
|
|
||||||
</h2>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
{#each record.fields as field}
|
|
||||||
<DynamicField
|
|
||||||
{field}
|
|
||||||
value={char.data[slugify(field.label)]}
|
|
||||||
data={char.data}
|
|
||||||
onChange={(v) => {
|
|
||||||
char.data[slugify(field.label)] = v;
|
|
||||||
roster.scheduleSave(char);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center gap-4 h-full min-h-[60vh]">
|
<div class="flex flex-col items-center justify-center gap-4 h-full min-h-[60vh]">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue