feat: add packer

This commit is contained in:
2026-10-03 23:11:51 +02:00
parent e82561862a
commit 5452f72087
14 changed files with 976 additions and 25 deletions
@@ -0,0 +1,140 @@
<script setup lang="ts">
const props = defineProps<{ category: PackerCategory, checked: Set<number> }>()
const emit = defineEmits<{
toggle: [id: number, value: boolean]
add: [name: string, weightG: number | null]
remove: [id: number]
weight: [id: number, weightG: number | null]
}>()
/** Grams typed into a field; empty or not a positive number is no weight */
function grams(text: string | number | null | undefined): number | null {
const value = Number(String(text ?? '').replace(',', '.'))
return value > 0 ? Math.round(value) : null
}
const newItem = ref('')
const newWeight = ref('')
function add() {
const name = newItem.value.trim()
if (!name) return
emit('add', name, grams(newWeight.value))
newItem.value = ''
newWeight.value = ''
}
// Weight of one piece, edited in place; an empty field goes back to the table's weight
const editing = ref<number | null>(null)
const draft = ref('')
function startEdit(entry: PackerEntry) {
editing.value = entry.id
draft.value = entry.weightG ? String(entry.weightG) : ''
}
function commit() {
if (editing.value === null) return
emit('weight', editing.value, grams(draft.value))
editing.value = null
}
</script>
<template>
<section class="mb-8 break-inside-avoid print:mb-5">
<h2 class="mb-2 text-sm font-semibold text-highlighted">
{{ props.category.name }}
</h2>
<ul class="flex flex-col gap-1.5">
<li v-for="entry in props.category.entries" :key="entry.id" class="group flex min-h-6 items-center gap-2">
<!-- print-color-adjust: browsers leave out the ticked box's fill when printing -->
<UCheckbox
:model-value="props.checked.has(entry.id)"
:ui="{ base: 'print:[print-color-adjust:exact]', label: props.checked.has(entry.id) ? 'text-muted line-through' : '' }"
@update:model-value="emit('toggle', entry.id, !!$event)"
>
<template #label>
<span v-if="entry.count > 1" class="tabular-nums">{{ entry.count }} × </span>{{ entry.name }}
</template>
</UCheckbox>
<UBadge v-if="entry.added" label="neu" color="neutral" variant="outline" size="sm" />
<UInput
v-if="editing === entry.id"
v-model="draft"
inputmode="numeric"
size="xs"
autofocus
:placeholder="entry.count > 1 ? 'g pro Stück' : 'g'"
:aria-label="`Gewicht von ${entry.name} in Gramm${entry.count > 1 ? ' pro Stück' : ''}`"
class="ms-auto w-24"
:ui="{ trailing: 'pe-2' }"
@keydown.enter="commit"
@keydown.esc="editing = null"
@blur="commit"
>
<template #trailing>
<span class="text-xs text-muted">g</span>
</template>
</UInput>
<!-- Click to change the weight for this session; changed weights are in the primary color -->
<button
v-else
type="button"
class="ms-auto rounded text-xs tabular-nums hover:text-highlighted focus-visible:outline-2 focus-visible:outline-primary"
:class="[
entry.weightEdited ? 'text-primary' : 'text-muted',
{ 'sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100 print:hidden': !entry.weightG }
]"
:title="entry.weightEdited ? 'Gewicht geändert (nur in dieser Sitzung)' : 'Gewicht ändern'"
:aria-label="`Gewicht von ${entry.name} ändern`"
@click="startEdit(entry)"
>
{{ entry.weightG ? packerWeight(entry.weightG * entry.count) : '+ g' }}
</button>
<!-- Always visible on touch screens, on hover with a mouse -->
<UButton
icon="i-lucide-x"
color="neutral"
variant="link"
size="xs"
:aria-label="`${entry.name} entfernen`"
class="sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100 print:hidden"
@click="emit('remove', entry.id)"
/>
</li>
</ul>
<form class="mt-2 flex gap-2 print:hidden" @submit.prevent="add">
<!-- The plus lines up with the checkboxes, the text with the item names -->
<UInput
v-model="newItem"
icon="i-lucide-plus"
placeholder="Hinzufügen"
variant="ghost"
size="sm"
:aria-label="`Zu ${props.category.name} hinzufügen`"
class="min-w-0 flex-1"
:ui="{ leading: 'ps-0', base: 'ps-6' }"
/>
<!-- Only once a name is typed, to keep the list calm -->
<UInput
v-if="newItem"
v-model="newWeight"
inputmode="numeric"
placeholder="Gewicht"
variant="ghost"
size="sm"
:aria-label="`Gewicht in Gramm`"
class="w-24"
:ui="{ trailing: 'pe-2' }"
>
<template #trailing>
<span class="text-xs text-muted">g</span>
</template>
</UInput>
<!-- Enter in either field adds the item -->
<button type="submit" class="hidden" />
</form>
</section>
</template>