Private
Public Access
1
0

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:
Catherine Renelle
2026-02-25 23:38:31 -05:00
parent 9b5708af64
commit 095d9756a8
33 changed files with 2226 additions and 143 deletions
+108
View File
@@ -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}