Add collapsible checklists on device detail, auto-collapse completed
Deploy to LXC / deploy (push) Successful in 19s

- Click chevron or checklist title to expand/collapse
- Completed checklists (100%) auto-collapse on page load
- Completed title shown in green with checkmark icon
- Progress bar and count always visible when collapsed
- Items and add-item form hidden when collapsed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 16:37:39 +07:00
parent 36f4d4b8d5
commit 99371648be
+51 -13
View File
@@ -15,6 +15,26 @@
let showImportChecklist = $state(false); let showImportChecklist = $state(false);
let editingChecklistId = $state<string | null>(null); let editingChecklistId = $state<string | null>(null);
let editingChecklistItemId = $state<string | null>(null); let editingChecklistItemId = $state<string | null>(null);
let collapsedChecklists = $state<Set<string>>(new Set());
// Auto-collapse completed checklists on load
$effect(() => {
const newCollapsed = new Set<string>();
for (const cl of data.checklists) {
if (cl.items.length > 0) {
const allDone = cl.items.every((i: any) => i.itemType === 'input' ? !!i.value : i.checked);
if (allDone) newCollapsed.add(cl.id);
}
}
collapsedChecklists = newCollapsed;
});
function toggleChecklist(id: string) {
const next = new Set(collapsedChecklists);
if (next.has(id)) next.delete(id);
else next.add(id);
collapsedChecklists = next;
}
function resetInput(formEl: HTMLFormElement) { function resetInput(formEl: HTMLFormElement) {
const input = formEl.querySelector('input[name="text"]') as HTMLInputElement; const input = formEl.querySelector('input[name="text"]') as HTMLInputElement;
const unit = formEl.querySelector('input[name="unit"]') as HTMLInputElement; const unit = formEl.querySelector('input[name="unit"]') as HTMLInputElement;
@@ -383,8 +403,30 @@
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
{#each data.checklists as checklist} {#each data.checklists as checklist}
{@const clCompleted = checklist.items.filter((i: any) => i.itemType === 'input' ? !!i.value : i.checked).length}
{@const clTotal = checklist.items.length}
{@const clPct = clTotal > 0 ? Math.round((clCompleted / clTotal) * 100) : 0}
{@const isCollapsed = collapsedChecklists.has(checklist.id)}
<div class="rounded-md border border-gray-100 p-3 dark:border-gray-700"> <div class="rounded-md border border-gray-100 p-3 dark:border-gray-700">
<div class="mb-2 flex items-center justify-between">
<!-- Header (always visible, clickable to toggle) -->
<div class="flex items-center justify-between">
<button type="button" onclick={() => toggleChecklist(checklist.id)}
class="flex flex-1 items-center gap-2 text-left">
<svg class="h-4 w-4 text-gray-400 transition-transform {isCollapsed ? '' : 'rotate-90'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
{#if editingChecklistId === checklist.id}
<!-- stop propagation handled by form click -->
{:else}
<h3 class="text-sm font-medium {clPct === 100 ? 'text-green-600 dark:text-green-400' : 'text-gray-900 dark:text-white'}">{checklist.title}</h3>
{#if clPct === 100}
<svg class="h-4 w-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{/if}
{/if}
</button>
{#if editingChecklistId === checklist.id} {#if editingChecklistId === checklist.id}
<form method="POST" action="?/renameChecklist" use:enhance={() => { <form method="POST" action="?/renameChecklist" use:enhance={() => {
return async ({ update, result }) => { return async ({ update, result }) => {
@@ -398,13 +440,9 @@
<button type="submit" class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">Save</button> <button type="submit" class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">Save</button>
<button type="button" onclick={() => (editingChecklistId = null)} class="text-sm text-gray-500 dark:text-gray-400">Cancel</button> <button type="button" onclick={() => (editingChecklistId = null)} class="text-sm text-gray-500 dark:text-gray-400">Cancel</button>
</form> </form>
{:else}
<h3 class="text-sm font-medium text-gray-900 dark:text-white">{checklist.title}</h3>
{/if} {/if}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-gray-400 dark:text-gray-500"> <span class="text-xs text-gray-400 dark:text-gray-500">{clCompleted}/{clTotal}</span>
{checklist.items.filter((i: any) => i.itemType === 'input' ? !!i.value : i.checked).length}/{checklist.items.length}
</span>
{#if editingChecklistId !== checklist.id} {#if editingChecklistId !== checklist.id}
<button type="button" onclick={() => (editingChecklistId = checklist.id)} class="text-gray-400 hover:text-blue-600 dark:text-gray-500 dark:hover:text-blue-400" title="Rename"> <button type="button" onclick={() => (editingChecklistId = checklist.id)} class="text-gray-400 hover:text-blue-600 dark:text-gray-500 dark:hover:text-blue-400" title="Rename">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -423,17 +461,16 @@
</div> </div>
</div> </div>
<!-- Progress bar --> <!-- Progress bar (always visible) -->
{#if checklist.items.length > 0} {#if clTotal > 0}
{@const completed = checklist.items.filter((i: any) => i.itemType === 'input' ? !!i.value : i.checked).length} <div class="mt-2 h-1.5 w-full rounded-full bg-gray-100 dark:bg-gray-700">
{@const pct = Math.round((completed / checklist.items.length) * 100)} <div class="h-1.5 rounded-full transition-all {clPct === 100 ? 'bg-green-500' : 'bg-blue-500'}" style="width: {clPct}%"></div>
<div class="mb-2 h-1.5 w-full rounded-full bg-gray-100 dark:bg-gray-700">
<div class="h-1.5 rounded-full transition-all {pct === 100 ? 'bg-green-500' : 'bg-blue-500'}" style="width: {pct}%"></div>
</div> </div>
{/if} {/if}
{#if !isCollapsed}
<!-- Items --> <!-- Items -->
<div class="space-y-1.5"> <div class="mt-3 space-y-1.5">
{#each checklist.items as item, idx} {#each checklist.items as item, idx}
{#if editingChecklistItemId === item.id} {#if editingChecklistItemId === item.id}
<!-- Inline edit --> <!-- Inline edit -->
@@ -555,6 +592,7 @@
</svg> </svg>
</button> </button>
</form> </form>
{/if}
</div> </div>
{/each} {/each}
</div> </div>