Private
Public Access
1
0

Fix checklist item ordering and add move up/down buttons

The BASE_CHARS in fractional-index.ts had digits after letters
(A-Za-z0-9), but PostgreSQL sorts digits before letters (0-9A-Za-z),
causing items to appear out of order once positions reached the digit
range. Reordered BASE_CHARS to match DB collation.

Also added up/down arrow buttons on checklist items so users can
reorder them within the list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-27 23:43:38 -05:00
parent bbc95a5bfc
commit df0e8909f2
2 changed files with 73 additions and 11 deletions
+71 -9
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { api } from '$lib/utils/api.js';
import { generatePosition } from '$lib/utils/fractional-index.js';
type ChecklistItem = { id: string; title: string; completed: boolean; position: string };
type Checklist = { id: string; title: string; position: string; items: ChecklistItem[] };
@@ -95,6 +96,41 @@
);
}
}
async function moveItem(clId: string, itemId: string, direction: 'up' | 'down') {
const cl = checklists.find((c) => c.id === clId);
if (!cl) return;
const idx = cl.items.findIndex((i) => i.id === itemId);
if (idx === -1) return;
if (direction === 'up' && idx === 0) return;
if (direction === 'down' && idx === cl.items.length - 1) return;
const targetIdx = direction === 'up' ? idx - 1 : idx + 1;
const before = direction === 'up'
? (targetIdx > 0 ? cl.items[targetIdx - 1].position : null)
: cl.items[targetIdx].position;
const after = direction === 'up'
? cl.items[targetIdx].position
: (targetIdx < cl.items.length - 1 ? cl.items[targetIdx + 1].position : null);
const newPos = generatePosition(before, after);
const { ok } = await api(`${basePath}/${clId}/items/${itemId}`, {
method: 'PATCH',
body: JSON.stringify({ position: newPos })
});
if (ok) {
const newItems = [...cl.items];
newItems[idx] = { ...newItems[idx], position: newPos };
// Re-sort by position
newItems.sort((a, b) => a.position.localeCompare(b.position));
onupdate(
checklists.map((c) =>
c.id === clId ? { ...c, items: newItems } : c
)
);
}
}
</script>
<div>
@@ -132,7 +168,7 @@
<!-- Items -->
<div class="space-y-1">
{#each cl.items as item}
{#each cl.items as item, idx}
<div class="flex items-center gap-2 group">
{#if canEdit}
<input
@@ -153,14 +189,40 @@
{item.title}
</span>
{#if canEdit}
<button
onclick={() => deleteItem(cl.id, item.id)}
class="text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition"
>
<svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition">
<button
onclick={() => moveItem(cl.id, item.id, 'up')}
disabled={idx === 0}
class="text-gray-300 dark:text-gray-600 hover:text-gray-500 dark:hover:text-gray-400 disabled:opacity-30 disabled:cursor-default p-0.5"
title="Move up"
aria-label="Move item up"
>
<svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
</button>
<button
onclick={() => moveItem(cl.id, item.id, 'down')}
disabled={idx === cl.items.length - 1}
class="text-gray-300 dark:text-gray-600 hover:text-gray-500 dark:hover:text-gray-400 disabled:opacity-30 disabled:cursor-default p-0.5"
title="Move down"
aria-label="Move item down"
>
<svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<button
onclick={() => deleteItem(cl.id, item.id)}
class="text-gray-300 dark:text-gray-600 hover:text-red-500 p-0.5"
title="Delete item"
aria-label="Delete item"
>
<svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
{/if}
</div>
{/each}
+2 -2
View File
@@ -8,8 +8,8 @@
* https://observablehq.com/@dgreensp/implementing-fractional-indexing
*/
const BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const MIDPOINT = 'N'; // roughly middle of uppercase range
const BASE_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const MIDPOINT = 'V'; // roughly middle of the charset
/**
* Generate a position string between `before` and `after`.