diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 509cfcd..1544027 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,6 +10,9 @@ const copied = ref(false) const items = ref([]) const newName = ref('') +const editingId = ref(null) +const editName = ref('') + const sublists = ref([]) const newSublistName = ref('') const newSublistColor = ref('#16a34a') @@ -180,24 +183,62 @@ async function removeItem(item) { items.value = items.value.filter((i) => i.id !== item.id) } +function startEdit(item) { + editingId.value = item.id + editName.value = item.name +} + +function cancelEdit() { + editingId.value = null +} + +function saveEdit(item) { + if (editingId.value !== item.id) return + editingId.value = null + const name = editName.value.trim() + if (!name || name === item.name) return + fetch(`/api/lists/${listId.value}/items/${item.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }) + .then((res) => res.json()) + .then((updated) => Object.assign(item, updated)) +} + const SWIPE_THRESHOLD = 70 const SWIPE_MAX = 140 +const LONG_PRESS_MS = 500 +const LONG_PRESS_TOLERANCE = 10 let drag = null +let longPressTimer = null -function onSwipeStart(e) { - drag = { startX: e.clientX, dx: 0, el: e.currentTarget } +function onSwipeStart(e, item) { + drag = { startX: e.clientX, startY: e.clientY, dx: 0, el: e.currentTarget } drag.el.setPointerCapture(e.pointerId) drag.el.style.transition = 'none' + + clearTimeout(longPressTimer) + longPressTimer = setTimeout(() => { + if (!drag) return + startEdit(item) + drag.el.style.transform = '' + drag = null + }, LONG_PRESS_MS) } function onSwipeMove(e) { if (!drag) return + if (Math.abs(e.clientX - drag.startX) > LONG_PRESS_TOLERANCE || Math.abs(e.clientY - drag.startY) > LONG_PRESS_TOLERANCE) { + clearTimeout(longPressTimer) + } const dx = e.clientX - drag.startX drag.dx = Math.max(-SWIPE_MAX, Math.min(SWIPE_MAX, dx)) drag.el.style.transform = `translateX(${drag.dx}px)` } function onSwipeEnd(e, item) { + clearTimeout(longPressTimer) if (!drag) return const { dx, el } = drag el.style.transition = 'transform 0.2s ease' @@ -371,6 +412,19 @@ function onHandleEnd() { item.position = position item.sublist_id = targetSublistId + // Reposition within the underlying array too — groups only filter it, they + // don't sort by position, so without this the item stays wherever it + // physically sat in the array until the next full refetch reorders it. + const globalIdx = items.value.findIndex((i) => i.id === item.id) + items.value.splice(globalIdx, 1) + const anchor = next ?? prev + if (anchor) { + const anchorIdx = items.value.findIndex((i) => i.id === anchor.id) + items.value.splice(next ? anchorIdx : anchorIdx + 1, 0, item) + } else { + items.value.push(item) + } + fetch(`/api/lists/${listId.value}/items/${item.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, @@ -412,7 +466,7 @@ onMounted(init)

skeps

-
+ - {{ item.name }} x{{ item.quantity }} + {{ item.name }} + @@ -635,9 +700,21 @@ body { main { max-width: 480px; margin: 2rem auto; - padding: env(safe-area-inset-top) 1rem env(safe-area-inset-bottom); + padding: calc(env(safe-area-inset-top) + 4.25rem) 1rem env(safe-area-inset-bottom); font-family: sans-serif; } +.add-item-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 20; + max-width: 480px; + margin: 0 auto; + padding: calc(env(safe-area-inset-top) + 0.75rem) 1rem 0.75rem; + background: var(--bg); + border-bottom: 1px solid var(--border); +} .theme-picker { display: block; font-size: 16px; @@ -754,6 +831,10 @@ ul { text-decoration: line-through; color: var(--muted); } +.edit-name { + flex: 1; + min-width: 0; +} .handle { touch-action: none; cursor: grab; @@ -791,9 +872,6 @@ ul { .add-sublist input[type='color']::-moz-color-swatch { border: none; } -.qty { - color: var(--muted); -} .settings { margin-top: 2rem; color: var(--muted);