Phase 4: Rich card features — labels, checklists, comments, attachments, categories
Add full CRUD + UI for all card sub-resources (tags/labels, assignees, checklists with items, comments with markdown, file attachments). Restructure CardModal into a two-column layout with lazy-loaded data. Upgrade card thumbnails with named label pills, urgency-colored due date badges, checklist progress indicators, and SVG icons. Add board categories with filtering on the listing page. Include markdown rendering with DOMPurify sanitization and .prose CSS styles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { renderMarkdown, initDOMPurify } from '$lib/utils/markdown.js';
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
readonly?: boolean;
|
||||
onsave?: (value: string) => void;
|
||||
};
|
||||
|
||||
let { value, readonly = false, onsave }: Props = $props();
|
||||
|
||||
let editing = $state(false);
|
||||
let draft = $state(value);
|
||||
let previewing = $state(false);
|
||||
let ready = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
await initDOMPurify();
|
||||
ready = true;
|
||||
});
|
||||
|
||||
function save() {
|
||||
onsave?.(draft);
|
||||
editing = false;
|
||||
previewing = false;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
draft = value;
|
||||
editing = false;
|
||||
previewing = false;
|
||||
}
|
||||
|
||||
// Keep draft in sync when value prop changes externally
|
||||
$effect(() => {
|
||||
if (!editing) draft = value;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if editing && !readonly}
|
||||
<div>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded {!previewing ? 'bg-gray-200 text-gray-700' : 'text-gray-500 hover:bg-gray-100'}"
|
||||
onclick={() => (previewing = false)}
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded {previewing ? 'bg-gray-200 text-gray-700' : 'text-gray-500 hover:bg-gray-100'}"
|
||||
onclick={() => (previewing = true)}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
{#if previewing}
|
||||
<div class="prose rounded-lg border border-gray-200 p-3 min-h-[120px] text-sm">
|
||||
{#if ready}
|
||||
{@html renderMarkdown(draft)}
|
||||
{:else}
|
||||
<p class="text-gray-400">{draft}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<textarea
|
||||
bind:value={draft}
|
||||
rows="6"
|
||||
class="w-full rounded-lg border border-gray-300 p-3 text-sm resize-y focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
|
||||
placeholder="Add a more detailed description... (Markdown supported)"
|
||||
></textarea>
|
||||
{/if}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button
|
||||
onclick={save}
|
||||
class="rounded bg-[var(--color-primary)] px-3 py-1.5 text-sm text-white hover:bg-[var(--color-primary-hover)] transition"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onclick={cancel}
|
||||
class="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="{!readonly ? 'cursor-pointer hover:bg-gray-50 rounded' : ''} p-2 -mx-2 min-h-[40px]"
|
||||
onclick={() => { if (!readonly) editing = true; }}
|
||||
>
|
||||
{#if value}
|
||||
<div class="prose text-sm">
|
||||
{#if ready}
|
||||
{@html renderMarkdown(value)}
|
||||
{:else}
|
||||
<p class="text-gray-700 whitespace-pre-wrap">{value}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if !readonly}
|
||||
<p class="text-sm text-gray-400">Add a more detailed description...</p>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-400 italic">No description</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user