mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-11 12:33:32 -04:00
feat: Improve new shopping list UI (#7600)
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -11,18 +11,28 @@
|
|||||||
>
|
>
|
||||||
<div class="d-flex flex-column ga-3">
|
<div class="d-flex flex-column ga-3">
|
||||||
<v-card-actions class="pa-0">
|
<v-card-actions class="pa-0">
|
||||||
<InputLabelType
|
<div class="position-relative" style="flex: 1;">
|
||||||
v-model="listItem.food"
|
<InputLabelType
|
||||||
v-model:item-id="listItem.foodId!"
|
ref="foodInputRef"
|
||||||
:items="foods"
|
v-model="listItem.food"
|
||||||
:label="rail ? $t('shopping-list.add-item') : $t('shopping-list.food')"
|
v-model:item-id="listItem.foodId!"
|
||||||
:icon="$globals.icons.foods"
|
:items="foods"
|
||||||
:style="rail ? 'margin-inline: 3px;' : undefined"
|
:label="rail ? $t('shopping-list.add-item') : $t('shopping-list.food')"
|
||||||
:search="rail"
|
:icon="$globals.icons.foods"
|
||||||
create
|
:style="rail ? 'margin-inline: 3px;' : undefined"
|
||||||
@create="createAssignFood"
|
:search="rail"
|
||||||
@focus="rail = false"
|
:menu-props="{ location: menuDirection }"
|
||||||
/>
|
create
|
||||||
|
@create="createAssignFood"
|
||||||
|
/>
|
||||||
|
<!-- Intercept clicks when collapsed so the drawer expands before the autocomplete opens -->
|
||||||
|
<div
|
||||||
|
v-if="rail"
|
||||||
|
class="position-absolute"
|
||||||
|
style="inset: 0; cursor: text;"
|
||||||
|
@click="expandAndFocus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<BaseButtonGroup
|
<BaseButtonGroup
|
||||||
v-if="!rail"
|
v-if="!rail"
|
||||||
:buttons="[
|
:buttons="[
|
||||||
@@ -84,6 +94,20 @@ defineEmits<{
|
|||||||
|
|
||||||
const { createAssignFood } = useShoppingListItemEditor(listItem);
|
const { createAssignFood } = useShoppingListItemEditor(listItem);
|
||||||
|
|
||||||
|
const { smAndDown } = useDisplay();
|
||||||
|
const menuDirection = computed(() => smAndDown.value ? "top" : "bottom");
|
||||||
|
|
||||||
|
const foodInputRef = ref<{ focus: () => void } | null>(null);
|
||||||
|
const rail = ref(true);
|
||||||
|
|
||||||
|
async function expandAndFocus() {
|
||||||
|
rail.value = false;
|
||||||
|
await nextTick();
|
||||||
|
setTimeout(() => {
|
||||||
|
foodInputRef.value?.focus();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => listItem.value.quantity,
|
() => listItem.value.quantity,
|
||||||
(newQty) => {
|
(newQty) => {
|
||||||
@@ -100,6 +124,4 @@ watch(
|
|||||||
listItem.value.labelId = listItem.value.label?.id || null;
|
listItem.value.labelId = listItem.value.label?.id || null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const rail = ref(true);
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
>
|
>
|
||||||
<v-row
|
<v-row
|
||||||
v-touch="{
|
v-touch="{
|
||||||
move: ({ originalEvent: { touches: [{ screenX }] } }) => {
|
move: ({ originalEvent: { touches: [{ screenX, screenY }] } }) => {
|
||||||
swipeInfo.touchendX = screenX;
|
swipeInfo.touchendX = screenX;
|
||||||
|
swipeInfo.touchendY = screenY;
|
||||||
},
|
},
|
||||||
start: ({ originalEvent: { touches: [{ screenX }] } }) => {
|
start: ({ originalEvent: { touches: [{ screenX, screenY }] } }) => {
|
||||||
swipeInfo.touchstartX = screenX;
|
swipeInfo.touchstartX = screenX;
|
||||||
|
swipeInfo.touchstartY = screenY;
|
||||||
},
|
},
|
||||||
end: () => {
|
end: () => {
|
||||||
if (swiping < SWIPE_THRESHOLD) {
|
if (swiping < SWIPE_THRESHOLD) {
|
||||||
@@ -212,6 +214,7 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const SWIPE_THRESHOLD = 50;
|
const SWIPE_THRESHOLD = 50;
|
||||||
|
const SCROLL_THRESHOLD = 50;
|
||||||
|
|
||||||
const { isRtl } = useRtl();
|
const { isRtl } = useRtl();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -264,14 +267,22 @@ function save() {
|
|||||||
edit.value = false;
|
edit.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const swipeInfo: Ref<{ touchstartX?: number; touchendX?: number }> = ref({ touchstartX: undefined, touchendX: undefined });
|
const swipeInfo: Ref<{ touchstartX?: number; touchendX?: number; touchstartY?: number; touchendY?: number }> = ref({});
|
||||||
const swiping = computed(() => {
|
const swiping = computed(() => {
|
||||||
const { touchstartX, touchendX } = swipeInfo.value ?? {};
|
const { touchstartX, touchendX, touchstartY, touchendY } = swipeInfo.value ?? {};
|
||||||
if (touchstartX === undefined || touchendX === undefined) {
|
if (touchstartX === undefined || touchendX === undefined) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const delta = isRtl.value ? touchstartX - touchendX : touchendX - touchstartX;
|
const deltaX = isRtl.value ? touchstartX - touchendX : touchendX - touchstartX;
|
||||||
return Math.min(Math.max(0, delta), 100);
|
|
||||||
|
// If there's significant vertical movement, treat as a scroll gesture and ignore
|
||||||
|
if (touchstartY !== undefined && touchendY !== undefined) {
|
||||||
|
const deltaY = Math.abs(touchendY - touchstartY);
|
||||||
|
if (deltaY > SCROLL_THRESHOLD) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(0, deltaX), 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeList = computed<RecipeSummary[]>(() => {
|
const recipeList = computed<RecipeSummary[]>(() => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
:items="units"
|
:items="units"
|
||||||
:label="$t('recipe.unit')"
|
:label="$t('recipe.unit')"
|
||||||
:icon="$globals.icons.units"
|
:icon="$globals.icons.units"
|
||||||
|
:menu-props="{ location: menuDirection }"
|
||||||
style="flex: 3"
|
style="flex: 3"
|
||||||
create
|
create
|
||||||
@create="createAssignUnit"
|
@create="createAssignUnit"
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
v-model:item-id="listItem.labelId!"
|
v-model:item-id="listItem.labelId!"
|
||||||
:items="labels"
|
:items="labels"
|
||||||
:label="$t('shopping-list.label')"
|
:label="$t('shopping-list.label')"
|
||||||
|
:menu-props="{ location: menuDirection }"
|
||||||
style="flex: 1 0 200px"
|
style="flex: 1 0 200px"
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
@@ -75,6 +77,9 @@ const emit = defineEmits<{ (e: "save"): void }>();
|
|||||||
|
|
||||||
const { assignLabelToFood, createAssignUnit } = useShoppingListItemEditor(listItem);
|
const { assignLabelToFood, createAssignUnit } = useShoppingListItemEditor(listItem);
|
||||||
|
|
||||||
|
const { smAndDown } = useDisplay();
|
||||||
|
const menuDirection = computed(() => smAndDown.value ? "top" : "bottom");
|
||||||
|
|
||||||
function handleNoteKeyPress(event: KeyboardEvent) {
|
function handleNoteKeyPress(event: KeyboardEvent) {
|
||||||
// Save on Enter
|
// Save on Enter
|
||||||
if (!event.shiftKey && event.key === "Enter") {
|
if (!event.shiftKey && event.key === "Enter") {
|
||||||
|
|||||||
@@ -93,4 +93,8 @@ function emitCreate() {
|
|||||||
emit("create", searchInput.value);
|
emit("create", searchInput.value);
|
||||||
autocompleteRef.value?.blur();
|
autocompleteRef.value?.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus: () => autocompleteRef.value?.focus(),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -943,7 +943,7 @@
|
|||||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?",
|
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?",
|
||||||
"no-shopping-lists-found": "No Shopping Lists Found",
|
"no-shopping-lists-found": "No Shopping Lists Found",
|
||||||
"item-checked-off": "{item} was checked off"
|
"item-checked-off": "Checked off {item}"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"all-recipes": "All Recipes",
|
"all-recipes": "All Recipes",
|
||||||
|
|||||||
@@ -377,20 +377,22 @@ const { store: allUnits } = useUnitStore();
|
|||||||
const { store: allFoods } = useFoodStore();
|
const { store: allFoods } = useFoodStore();
|
||||||
|
|
||||||
function itemCheckedToast(item: ShoppingListItemOut) {
|
function itemCheckedToast(item: ShoppingListItemOut) {
|
||||||
alert.info(
|
setTimeout(() => {
|
||||||
i18n.t("shopping-list.item-checked-off", { item: item.food?.name || item.note || i18n.t("recipe.ingredient") }),
|
alert.info(
|
||||||
undefined,
|
i18n.t("shopping-list.item-checked-off", { item: item.food?.name || item.note || i18n.t("recipe.ingredient") }),
|
||||||
{
|
undefined,
|
||||||
timeout: 4000,
|
{
|
||||||
action: {
|
timeout: 4000,
|
||||||
message: i18n.t("general.undo"),
|
action: {
|
||||||
onClick: () => {
|
message: i18n.t("general.undo"),
|
||||||
item.checked = false;
|
onClick: () => {
|
||||||
shoppingListPage.saveListItem(item);
|
item.checked = false;
|
||||||
|
shoppingListPage.saveListItem(item);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
);
|
||||||
);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
Reference in New Issue
Block a user