refactor(fend): character-level operations on the left, global operations on the right
This commit is contained in:
parent
ef0c982d98
commit
b7fe539cf0
4 changed files with 77 additions and 59 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Trash2 } from 'lucide-svelte';
|
|
||||||
import { roster } from '$lib/state.svelte';
|
import { roster } from '$lib/state.svelte';
|
||||||
import { slugify } from '$lib/utils/slugify';
|
import { slugify } from '$lib/utils/slugify';
|
||||||
|
|
||||||
|
|
@ -9,30 +8,18 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="relative inline-block">
|
||||||
<select
|
<select
|
||||||
value={roster.active?.id ?? ''}
|
value={roster.active?.id ?? ''}
|
||||||
onchange={(e) => roster.setActive((e.target as HTMLSelectElement).value)}
|
onchange={(e) => roster.setActive((e.target as HTMLSelectElement).value)}
|
||||||
class="rounded px-2 py-1 text-sm"
|
class="rounded pl-3 pr-7 h-[30px] text-sm appearance-none cursor-pointer hover:opacity-80"
|
||||||
style="background: var(--bg-input); border: 1px solid var(--border); color: var(--text);"
|
style="background: var(--bg-card); border: 1px solid var(--border); color: var(--text);"
|
||||||
>
|
>
|
||||||
{#each roster.characters as char}
|
{#each roster.characters as char}
|
||||||
<option value={char.id}>{displayName(char)}</option>
|
<option value={char.id}>{displayName(char)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
<svg class="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none" width="12" height="12" viewBox="0 0 12 12" style="color: var(--text-muted);">
|
||||||
{#if roster.active}
|
<path d="M3 5l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<button
|
</svg>
|
||||||
onclick={async () => {
|
|
||||||
if (roster.active && confirm('Delete this character?')) {
|
|
||||||
await roster.remove(roster.active.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class="p-1 rounded hover:opacity-80"
|
|
||||||
style="color: var(--text-muted);"
|
|
||||||
title="Delete character"
|
|
||||||
>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,78 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Sun, Moon } from 'lucide-svelte';
|
import { Sun, Moon, Share2, Check, Trash2 } from 'lucide-svelte';
|
||||||
import { theme } from '$lib/theme.svelte';
|
import { theme } from '$lib/theme.svelte';
|
||||||
import { roster } from '$lib/state.svelte';
|
import { roster } from '$lib/state.svelte';
|
||||||
import { presets } from '$lib/presets';
|
import { presets } from '$lib/presets';
|
||||||
|
import { encodeCharacterURL } from '$lib/sharing';
|
||||||
|
import { slugify } from '$lib/utils/slugify';
|
||||||
import CharacterSwitcher from './CharacterSwitcher.svelte';
|
import CharacterSwitcher from './CharacterSwitcher.svelte';
|
||||||
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
|
let shared = $state(false);
|
||||||
|
let confirmDelete = $state(false);
|
||||||
|
|
||||||
async function createCharacter() {
|
async function createCharacter() {
|
||||||
await roster.create(presets[0]);
|
await roster.create(presets[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function share() {
|
||||||
|
const char = roster.active;
|
||||||
|
if (!char) return;
|
||||||
|
const encoded = encodeCharacterURL(char);
|
||||||
|
const url = `${window.location.origin}${window.location.pathname}#${encoded}`;
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
shared = true;
|
||||||
|
setTimeout(() => { shared = false; }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayName(): string {
|
||||||
|
const char = roster.active;
|
||||||
|
if (!char) return '';
|
||||||
|
const name = char.data[slugify('Name')];
|
||||||
|
return (name as string) || 'Unnamed Character';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDelete() {
|
||||||
|
if (roster.active) {
|
||||||
|
await roster.remove(roster.active.id);
|
||||||
|
}
|
||||||
|
confirmDelete = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="flex items-center gap-3 px-4 py-3 border-b shrink-0" style="border-color: var(--border); background: var(--bg-card);">
|
<header class="flex items-center gap-2 px-4 py-3 border-b shrink-0" style="border-color: var(--border); background: var(--bg-card);">
|
||||||
<h1 class="font-bold whitespace-nowrap">Aurora Records</h1>
|
<h1 class="font-bold whitespace-nowrap">Aurora Records</h1>
|
||||||
|
|
||||||
{#if roster.characters.length > 0}
|
{#if roster.characters.length > 0}
|
||||||
<CharacterSwitcher />
|
<CharacterSwitcher />
|
||||||
|
|
||||||
|
<button onclick={share} class="flex items-center justify-center w-[30px] h-[30px] rounded border hover:opacity-80" style="border-color: var(--border);" title="Share character">
|
||||||
|
{#if shared}
|
||||||
|
<Check size={14} />
|
||||||
|
{:else}
|
||||||
|
<Share2 size={14} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick={() => { confirmDelete = true; }} class="flex items-center justify-center w-[30px] h-[30px] rounded border hover:opacity-80" style="border-color: var(--border);" title="Delete character">
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-3">
|
<div class="ml-auto flex items-center gap-2">
|
||||||
<span class="text-sm" style="color: var(--text-muted);">
|
<span class="text-sm" style="color: var(--text-muted);">
|
||||||
{#if roster.saveStatus === 'saving'}Saving...{:else if roster.saveStatus === 'saved'}Saved{/if}
|
{#if roster.saveStatus === 'saving'}Saving...{:else if roster.saveStatus === 'saved'}Saved{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={createCharacter}
|
onclick={createCharacter}
|
||||||
class="px-3 py-1 rounded text-sm border hover:opacity-80"
|
class="px-3 h-[30px] rounded text-sm border hover:opacity-80"
|
||||||
style="border-color: var(--border);"
|
style="border-color: var(--border);"
|
||||||
>
|
>
|
||||||
New Character
|
New Character
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onclick={() => theme.toggle()} class="p-1 rounded hover:opacity-80" title="Toggle theme">
|
<button onclick={() => theme.toggle()} class="flex items-center justify-center w-[30px] h-[30px] rounded hover:opacity-80" title="Toggle theme">
|
||||||
{#if theme.dark}
|
{#if theme.dark}
|
||||||
<Sun size={18} />
|
<Sun size={18} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -39,3 +81,18 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if confirmDelete && roster.active}
|
||||||
|
<Modal onClose={() => { confirmDelete = false; }}>
|
||||||
|
<h2 class="font-semibold mb-2">Delete Character</h2>
|
||||||
|
<p class="text-sm mb-4">Delete <strong>{displayName()}</strong>? This can't be undone.</p>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button onclick={() => { confirmDelete = false; }} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border);">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button onclick={doDelete} class="px-3 py-1 rounded text-sm border hover:opacity-80" style="border-color: var(--border); color: #dc2626;">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Character } from '$lib/types';
|
import type { Character } from '$lib/types';
|
||||||
import { generateRecord } from '$lib/output';
|
import { generateRecord } from '$lib/output';
|
||||||
import { encodeCharacterURL } from '$lib/sharing';
|
|
||||||
import { species } from '$lib/data';
|
import { species } from '$lib/data';
|
||||||
import OutputTab from './OutputTab.svelte';
|
import OutputTab from './OutputTab.svelte';
|
||||||
|
|
||||||
|
|
@ -22,12 +21,6 @@
|
||||||
let output = $derived(
|
let output = $derived(
|
||||||
activeTab ? generateRecord(character.template, character.data, activeTab, species) : ''
|
activeTab ? generateRecord(character.template, character.data, activeTab, species) : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
async function share() {
|
|
||||||
const encoded = encodeCharacterURL(character);
|
|
||||||
const url = `${window.location.origin}${window.location.pathname}#${encoded}`;
|
|
||||||
await navigator.clipboard.writeText(url);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full min-h-0 rounded border" style="border-color: var(--border); background: var(--bg-card);">
|
<div class="flex flex-col h-full min-h-0 rounded border" style="border-color: var(--border); background: var(--bg-card);">
|
||||||
|
|
@ -45,5 +38,5 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OutputTab {output} onShare={share} />
|
<OutputTab {output} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Copy, Check, Share2 } from 'lucide-svelte';
|
import { Copy, Check } from 'lucide-svelte';
|
||||||
|
|
||||||
let { output, onShare }: { output: string; onShare?: () => void } = $props();
|
let { output }: { output: string } = $props();
|
||||||
|
|
||||||
let copied = $state(false);
|
let copied = $state(false);
|
||||||
let shared = $state(false);
|
|
||||||
|
|
||||||
let wordCount = $derived(
|
let wordCount = $derived(
|
||||||
output.trim() ? output.trim().split(/\s+/).length : 0
|
output.trim() ? output.trim().split(/\s+/).length : 0
|
||||||
|
|
@ -15,36 +14,18 @@
|
||||||
copied = true;
|
copied = true;
|
||||||
setTimeout(() => { copied = false; }, 2000);
|
setTimeout(() => { copied = false; }, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function share() {
|
|
||||||
if (!onShare) return;
|
|
||||||
await onShare();
|
|
||||||
shared = true;
|
|
||||||
setTimeout(() => { shared = false; }, 2000);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full min-h-0">
|
<div class="flex flex-col h-full min-h-0">
|
||||||
<div class="flex items-center justify-between px-3 py-2 text-sm shrink-0" style="color: var(--text-muted);">
|
<div class="flex items-center justify-between px-3 py-2 text-sm shrink-0" style="color: var(--text-muted);">
|
||||||
<span>{wordCount} words</span>
|
<span>{wordCount} words</span>
|
||||||
<div class="flex items-center gap-2">
|
<button onclick={copy} class="flex items-center gap-1 px-2 py-1 rounded border hover:opacity-80" style="border-color: var(--border);">
|
||||||
<button onclick={copy} class="flex items-center gap-1 px-2 py-1 rounded border hover:opacity-80" style="border-color: var(--border);">
|
{#if copied}
|
||||||
{#if copied}
|
<Check size={14} /> Copied
|
||||||
<Check size={14} /> Copied
|
{:else}
|
||||||
{:else}
|
<Copy size={14} /> Copy
|
||||||
<Copy size={14} /> Copy
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{#if onShare}
|
|
||||||
<button onclick={share} class="flex items-center gap-1 px-2 py-1 rounded border hover:opacity-80" style="border-color: var(--border);">
|
|
||||||
{#if shared}
|
|
||||||
<Check size={14} /> Link Copied
|
|
||||||
{:else}
|
|
||||||
<Share2 size={14} /> Share
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre class="flex-1 overflow-auto px-4 py-3 text-sm whitespace-pre-wrap font-mono" style="background: var(--bg); color: var(--text);">{output}</pre>
|
<pre class="flex-1 overflow-auto px-4 py-3 text-sm whitespace-pre-wrap font-mono" style="background: var(--bg); color: var(--text);">{output}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue