Add collapsible checklists on device detail, auto-collapse completed
Deploy to LXC / deploy (push) Successful in 19s
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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user