ux: unify UI based on user-feedback (#1216)

* unify UI based on user-feedback

* fix layout shify error

* implement drag and drop animation
This commit is contained in:
Hayden 2022-05-11 17:14:03 -08:00 committed by GitHub
parent 8f1c082d79
commit 4fe19b88ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 282 additions and 194 deletions

View File

@ -24,19 +24,21 @@
</div>
</v-expand-transition>
</RecipeCardImage>
<v-card-title class="my-n3 mb-n6">
<v-card-title class="my-n3 px-2 mb-n6">
<div class="headerClass">
{{ name }}
</div>
</v-card-title>
<slot name="actions">
<v-card-actions>
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
<v-card-actions class="px-1">
<RecipeFavoriteBadge v-if="loggedIn" class="absolute" :slug="slug" show-always />
<RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" />
<v-spacer></v-spacer>
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
<RecipeContextMenu
color="grey darken-2"
:slug="slug"
:name="name"
:recipe-id="recipeId"

View File

@ -40,6 +40,7 @@
<script lang="ts">
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import colors from "vuetify/lib/util/colors";
import { useUserApi } from "~/composables/api";
export interface ContextMenuIncludes {
@ -91,7 +92,7 @@ export default defineComponent({
},
color: {
type: String,
default: "primary",
default: colors.grey.darken2,
},
slug: {
type: String,

View File

@ -8,7 +8,7 @@
</template>
<v-card>
<v-app-bar dark color="primary" class="mt-n1 mb-3">
<v-app-bar dense dark color="primary" class="mb-2">
<v-icon large left>
{{ $globals.icons.createAlt }}
</v-icon>
@ -21,34 +21,26 @@
v-model="inputText"
outlined
rows="12"
hide-details
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
>
</v-textarea>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn outlined color="info" small v-bind="attrs" @click="trimAllLines" v-on="on">
Trim Whitespace
</v-btn>
</template>
<span> Trim leading and trailing whitespace as well as blank lines </span>
</v-tooltip>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="removeFirstCharacter" v-on="on">
Trim Prefix
</v-btn>
<v-divider></v-divider>
<template v-for="(util, idx) in utilities">
<v-list-item :key="util.id" dense class="py-1">
<v-list-item-title>
<v-list-item-subtitle class="wrap-word">
{{ util.description }}
</v-list-item-subtitle>
</v-list-item-title>
<BaseButton small color="info" @click="util.action">
<template #icon> {{ $globals.icons.robot }}</template>
Run
</BaseButton>
</v-list-item>
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
</template>
<span> Trim first character from each line </span>
</v-tooltip>
<v-tooltip top>
<template #activator="{ on, attrs }">
<v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="splitByNumberedLine" v-on="on">
Split By Numbered Line
</v-btn>
</template>
<span> Attempts to split a paragraph by matching 1) or 1. patterns </span>
</v-tooltip>
</v-card-text>
<v-divider></v-divider>
@ -64,7 +56,7 @@
</template>
<script lang="ts">
import { reactive, toRefs, defineComponent } from "@nuxtjs/composition-api";
import { reactive, toRefs, defineComponent, useContext } from "@nuxtjs/composition-api";
export default defineComponent({
setup(_, context) {
const state = reactive({
@ -78,7 +70,7 @@ export default defineComponent({
function removeFirstCharacter() {
state.inputText = splitText()
.map((line) => line.substr(1))
.map((line) => line.substring(1))
.join("\n");
}
@ -109,7 +101,28 @@ export default defineComponent({
state.dialog = false;
}
const { i18n } = useContext();
const utilities = [
{
id: "trim-whitespace",
description: i18n.tc("new-recipe.trim-whitespace-description"),
action: trimAllLines,
},
{
id: "trim-prefix",
description: i18n.tc("new-recipe.trim-prefix-description"),
action: removeFirstCharacter,
},
{
id: "split-by-numbered-line",
description: i18n.tc("new-recipe.split-by-numbered-line-description"),
action: splitByNumberedLine,
},
];
return {
utilities,
splitText,
trimAllLines,
removeFirstCharacter,

View File

@ -21,18 +21,12 @@
type="number"
placeholder="Quantity"
>
<v-icon
v-if="$listeners && $listeners.delete"
slot="prepend"
class="mr-n1"
color="error"
@click="$emit('delete')"
>
{{ $globals.icons.delete }}
<v-icon v-if="$listeners && $listeners.delete" slot="prepend" class="mr-n1 handle">
{{ $globals.icons.arrowUpDown }}
</v-icon>
</v-text-field>
</v-col>
<v-col v-if="!disableAmount && units" sm="12" md="3" cols="12">
<v-col v-if="!disableAmount" sm="12" md="3" cols="12">
<v-autocomplete
v-model="value.unit"
:search-input.sync="unitSearch"
@ -40,7 +34,7 @@
dense
solo
return-object
:items="units"
:items="units || []"
item-text="name"
class="mx-1"
placeholder="Choose Unit"
@ -59,7 +53,7 @@
</v-col>
<!-- Foods Input -->
<v-col v-if="!disableAmount && foods" m="12" md="3" cols="12" class="">
<v-col v-if="!disableAmount" m="12" md="3" cols="12" class="">
<v-autocomplete
v-model="value.food"
:search-input.sync="foodSearch"
@ -67,7 +61,7 @@
dense
solo
return-object
:items="foods"
:items="foods || []"
item-text="name"
class="mx-1 py-0"
placeholder="Choose Food"
@ -85,28 +79,34 @@
</v-autocomplete>
</v-col>
<v-col sm="12" md="" cols="12">
<div class="d-flex">
<v-text-field v-model="value.note" hide-details dense solo class="mx-1" placeholder="Notes">
<v-icon v-if="disableAmount" slot="prepend" class="mr-n1" color="error" @click="$emit('delete')">
{{ $globals.icons.delete }}
<v-icon v-if="disableAmount && $listeners && $listeners.delete" slot="prepend" class="mr-n1 handle">
{{ $globals.icons.arrowUpDown }}
</v-icon>
<template slot="append-outer">
</v-text-field>
<BaseButtonGroup
hover
:large="false"
class="handle my-auto"
class="my-auto"
:buttons="[
{
icon: $globals.icons.arrowUpDown,
text: '',
icon: $globals.icons.delete,
text: 'Delete',
event: 'delete',
},
{
icon: $globals.icons.dotsVertical,
text: 'Menu',
event: 'open',
children: contextMenuOptions,
},
]"
@toggle-section="toggleTitle"
@toggle-original="toggleOriginalText"
@delete="$emit('delete')"
/>
</template>
</v-text-field>
</div>
</v-col>
</v-row>
<p v-if="showOriginalText" class="text-caption">Original Text: {{ value.originalText }}</p>

View File

@ -71,11 +71,17 @@
:disabled="!edit"
:value="value"
handle=".handle"
v-bind="{
animation: 200,
group: 'description',
ghostClass: 'ghost',
}"
@input="updateIndex"
@start="drag = true"
@end="drag = false"
>
<div v-for="(step, index) in value" :key="step.id">
<TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''">
<div v-for="(step, index) in value" :key="step.id" class="list-group-item">
<v-app-bar
v-if="showTitleEditor[step.id]"
class="primary mx-1 mt-6"
@ -109,17 +115,20 @@
@click="toggleDisabled(index)"
>
<v-card-title :class="{ 'pb-0': !isChecked(index) }">
<v-btn v-if="edit" fab x-small color="white" class="mr-2" elevation="0" @click="value.splice(index, 1)">
<v-icon size="24" color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
<span class="handle">
<v-icon v-if="edit" size="26" class="pb-1">{{ $globals.icons.arrowUpDown }}</v-icon>
{{ $t("recipe.step-index", { step: index + 1 }) }}
</span>
<template v-if="edit">
<v-icon class="handle ml-auto mr-2">{{ $globals.icons.arrowUpDown }}</v-icon>
<div>
<div class="ml-auto">
<BaseButtonGroup
:large="false"
:buttons="[
{
icon: $globals.icons.delete,
text: $tc('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.dotsVertical,
text: '',
@ -137,22 +146,22 @@
text: 'Merge Above',
event: 'merge-above',
},
],
},
{
icon: previewStates[index] ? $globals.icons.edit : $globals.icons.eye,
text: previewStates[index] ? $tc('general.edit') : 'Preview Markdown',
text: previewStates[index] ? 'Edit Markdown' : 'Preview Markdown',
event: 'preview-step',
},
],
},
]"
@merge-above="mergeAbove(index - 1, index)"
@toggle-section="toggleShowTitle(step.id)"
@link-ingredients="openDialog(index, step.ingredientReferences, step.text)"
@preview-step="togglePreviewState(index)"
@delete="value.splice(index, 1)"
/>
</div>
</template>
<v-fade-transition>
<v-icon v-show="isChecked(index)" size="24" class="ml-auto" color="success">
{{ $globals.icons.checkboxMarkedCircle }}
@ -181,6 +190,7 @@
</v-card>
</v-hover>
</div>
</TransitionGroup>
</draggable>
</section>
</template>
@ -481,7 +491,10 @@ export default defineComponent({
}
}
const drag = ref(false);
return {
drag,
togglePreviewState,
toggleCollapseSection,
previewStates,
@ -521,4 +534,23 @@ export default defineComponent({
.markdown >>> ol > li {
display: list-item;
}
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
}
.list-group {
min-height: 38px;
}
.list-group-item {
cursor: move;
}
.list-group-item i {
cursor: pointer;
}
</style>

View File

@ -1,29 +1,27 @@
<template>
<div v-if="value.length > 0 || edit">
<div v-if="value.length > 0 || edit" class="mt-8">
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
<v-card v-for="(note, index) in value" :key="'note' + index" class="mt-1">
<div v-if="edit">
<div v-for="(note, index) in value" :key="'note' + index" class="mt-1">
<v-card v-if="edit">
<v-card-text>
<v-row align="center">
<v-btn fab x-small color="white" class="mr-2" elevation="0" @click="removeByIndex(value, index)">
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
<div class="d-flex align-center">
<v-text-field v-model="value[index]['title']" :label="$t('recipe.title')" />
<v-btn icon class="mr-2" elevation="0" @click="removeByIndex(value, index)">
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
<v-text-field v-model="value[index]['title']" :label="$t('recipe.title')"></v-text-field>
</v-row>
<v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')"> </v-textarea>
</v-card-text>
</div>
<v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')" />
</v-card-text>
</v-card>
<div v-else>
<v-card-title class="py-2">
<v-card-title class="text-subtitle-1 font-weight-medium py-1">
{{ note.title }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<VueMarkdown :source="note.text"> </VueMarkdown>
</v-card-text>
</div>
</v-card>
</div>
<div v-if="edit" class="d-flex justify-end">
<BaseButton class="ml-auto my-2" @click="addNote"> {{ $t("general.new") }}</BaseButton>

View File

@ -1,7 +1,7 @@
<template>
<v-item-group>
<template v-for="btn in buttons">
<v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-x left>
<v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-y top left>
<template #activator="{ on, attrs }">
<v-btn tile :large="large" icon v-bind="attrs" v-on="on">
<v-icon>

View File

@ -215,7 +215,10 @@
"upload-a-recipe": "Upload a Recipe",
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
"url-form-hint": "Copy and paste a link from your favorite recipe website",
"view-scraped-data": "View Scraped Data"
"view-scraped-data": "View Scraped Data",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
},
"page": {
"404-page-not-found": "404 Page not found",

View File

@ -127,14 +127,29 @@
<!-- Advanced Editor -->
<div v-if="form">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<draggable v-if="recipe.recipeIngredient.length > 0" v-model="recipe.recipeIngredient" handle=".handle">
<draggable
v-if="recipe.recipeIngredient.length > 0"
v-model="recipe.recipeIngredient"
handle=".handle"
v-bind="{
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost',
}"
@start="drag = true"
@end="drag = false"
>
<TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''">
<RecipeIngredientEditor
v-for="(ingredient, index) in recipe.recipeIngredient"
:key="ingredient.referenceId"
v-model="recipe.recipeIngredient[index]"
class="list-group-item"
:disable-amount="recipe.settings.disableAmount"
@delete="recipe.recipeIngredient.splice(index, 1)"
/>
</TransitionGroup>
</draggable>
<v-skeleton-loader v-else boilerplate elevation="2" type="list-item"> </v-skeleton-loader>
<div class="d-flex justify-end mt-2">
@ -820,8 +835,11 @@ export default defineComponent({
return "Parse ingredients";
});
const drag = ref(false);
return {
// Wake Lock
drag,
wakeIsSupported,
isActive,
lockScreen,
@ -857,3 +875,24 @@ export default defineComponent({
head: {},
});
</script>
<style lang="css">
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
}
.list-group {
min-height: 38px;
}
.list-group-item {
cursor: move;
}
.list-group-item i {
cursor: pointer;
}
</style>