Files
root-org/src/lib/components/kanban/KanbanCard.svelte
AlacrisDevs 819d5b876a ui: overhaul files, kanban, calendar, settings, chat modules
- FileBrowser: modernize breadcrumbs, toolbar, list/grid items, empty states
- KanbanColumn: remove fixed height, border-based styling, compact header
- KanbanCard: cleaner border styling, smaller tags, compact footer
- Calendar: compact nav bar, border grid, today circle indicator, day view empty state
- DocumentViewer: remove bg-night rounded-[32px], border-b header pattern
- Settings tags: inline border/rounded-xl cards, icon action buttons
- Chat: create +layout.svelte with PageHeader, overhaul sidebar and main area
- Chat i18n: add nav_chat, chat_title, chat_subtitle keys (en + et)

svelte-check: 0 errors, vitest: 112/112 passed
2026-02-07 11:03:58 +02:00

137 lines
3.3 KiB
Svelte

<script lang="ts">
import type { KanbanCard as KanbanCardType } from "$lib/supabase/types";
import { Avatar } from "$lib/components/ui";
interface Tag {
id: string;
name: string;
color: string;
}
interface Props {
card: KanbanCardType & {
tags?: Tag[];
checklist_done?: number;
checklist_total?: number;
assignee_name?: string | null;
assignee_avatar?: string | null;
};
isDragging?: boolean;
onclick?: () => void;
ondelete?: (cardId: string) => void;
draggable?: boolean;
ondragstart?: (e: DragEvent) => void;
}
let {
card,
isDragging = false,
onclick,
ondelete,
draggable = true,
ondragstart,
}: Props = $props();
function handleDelete(e: MouseEvent) {
e.stopPropagation();
if (confirm("Are you sure you want to delete this card?")) {
ondelete?.(card.id);
}
}
function formatDueDate(dateStr: string | null): string {
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
const hasFooter = $derived(
!!card.due_date ||
(card.checklist_total ?? 0) > 0 ||
!!card.assignee_id,
);
</script>
<button
type="button"
class="bg-night/80 border border-light/5 hover:border-light/10 rounded-xl px-3 py-2.5 cursor-pointer transition-all group w-full text-left flex flex-col gap-1.5 relative"
class:opacity-50={isDragging}
{draggable}
{ondragstart}
{onclick}
>
<!-- Delete button (top-right, visible on hover) -->
{#if ondelete}
<button
type="button"
class="absolute top-1.5 right-1.5 p-0.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-error/10 transition-all z-10"
onclick={handleDelete}
aria-label="Delete card"
>
<span
class="material-symbols-rounded text-light/30 hover:text-error"
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
>
close
</span>
</button>
{/if}
<!-- Tags / Chips -->
{#if card.tags && card.tags.length > 0}
<div class="flex gap-1 items-start flex-wrap">
{#each card.tags as tag}
<span
class="rounded-[4px] px-1.5 py-0.5 font-body font-bold text-[11px] text-night leading-none"
style="background-color: {tag.color || '#00A3E0'}"
>
{tag.name}
</span>
{/each}
</div>
{/if}
<!-- Title -->
<p class="font-body text-body-sm text-white w-full leading-snug">
{card.title}
</p>
<!-- Bottom row: details + avatar -->
{#if hasFooter}
<div class="flex items-center justify-between w-full mt-0.5">
<div class="flex gap-2 items-center text-[11px] text-light/40">
{#if card.due_date}
<span class="flex items-center gap-0.5">
<span
class="material-symbols-rounded"
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
>calendar_today</span>
{formatDueDate(card.due_date)}
</span>
{/if}
{#if (card.checklist_total ?? 0) > 0}
<span class="flex items-center gap-0.5">
<span
class="material-symbols-rounded"
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
>check_box</span>
{card.checklist_done ?? 0}/{card.checklist_total}
</span>
{/if}
</div>
{#if card.assignee_id}
<Avatar
name={card.assignee_name || "?"}
src={card.assignee_avatar}
size="xs"
/>
{/if}
</div>
{/if}
</button>