feat(frontend): Add animations for form elements

This commit is contained in:
2026-02-15 12:23:30 +01:00
parent 05b89778cf
commit a931275d09
3 changed files with 177 additions and 123 deletions

View File

@@ -49,7 +49,11 @@
/* Global focus styles for elements without built-in focus styling. /* Global focus styles for elements without built-in focus styling.
Nuxt UI components (inputs, buttons, selects, etc.) have their own Nuxt UI components (inputs, buttons, selects, etc.) have their own
focus ring styling, so we exclude them to avoid double borders. */ focus ring styling, so we exclude them to avoid double borders. */
:focus-visible:not(input):not(textarea):not(select):not(button):not([role="combobox"]):not([role="listbox"]):not([role="option"]):not([role="checkbox"]):not([role="radio"]):not([role="switch"]):not([role="slider"]):not([role="tab"]):not([role="menuitem"]) { :focus-visible:not(input):not(textarea):not(select):not(button):not([role='combobox']):not([role='listbox']):not(
[role='option']
):not([role='checkbox']):not([role='radio']):not([role='switch']):not([role='slider']):not([role='tab']):not(
[role='menuitem']
) {
outline: 2px solid var(--color-teal-500); outline: 2px solid var(--color-teal-500);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -95,3 +99,38 @@
.dark .nav-separator { .dark .nav-separator {
background: linear-gradient(90deg, transparent, var(--color-gray-700), transparent); background: linear-gradient(90deg, transparent, var(--color-gray-700), transparent);
} }
/* ============================================
FORM ELEMENT VISIBILITY TRANSITIONS
============================================ */
/* Subtle fade + slide animation for form elements appearing/disappearing */
.form-element-enter-active {
transition:
opacity 200ms ease-out,
transform 200ms ease-out;
}
.form-element-leave-active {
transition:
opacity 200ms ease-out,
transform 200ms ease-out;
/* Take leaving elements out of flow so remaining elements reposition smoothly */
position: absolute;
width: 100%;
}
.form-element-enter-from {
opacity: 0;
transform: translateY(-8px);
}
.form-element-leave-to {
opacity: 0;
transform: translateY(8px);
}
/* Smooth repositioning when elements are added/removed */
.form-element-move {
transition: transform 200ms ease-out;
}

View File

@@ -1,7 +1,9 @@
<template> <template>
<template <TransitionGroup name="form-element" tag="div" class="relative" @before-enter="applyCascadingEntryDelay">
<div
v-for="(formElementItem, visibleIndex) in visibleFormElements" v-for="(formElementItem, visibleIndex) in visibleFormElements"
:key="getElementKey(formElementItem.formElement, formElementItem.indexInSubsection)" :key="getElementKey(formElementItem.formElement, formElementItem.indexInSubsection)"
:data-index="visibleIndex"
> >
<div class="group flex py-3 lg:py-4"> <div class="group flex py-3 lg:py-4">
<div class="flex-auto min-w-0"> <div class="flex-auto min-w-0">
@@ -36,7 +38,9 @@
</div> </div>
<TheComment <TheComment
v-if=" v-if="
applicationFormId && formElementItem.formElement.id && activeFormElement === formElementItem.formElement.id applicationFormId &&
formElementItem.formElement.id &&
activeFormElement === formElementItem.formElement.id
" "
:form-element-id="formElementItem.formElement.id" :form-element-id="formElementItem.formElement.id"
:application-form-id="applicationFormId" :application-form-id="applicationFormId"
@@ -96,7 +100,8 @@
</div> </div>
</div> </div>
<USeparator v-if="visibleIndex < visibleFormElements.length - 1" /> <USeparator v-if="visibleIndex < visibleFormElements.length - 1" />
</template> </div>
</TransitionGroup>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -245,4 +250,9 @@ async function toggleComments(formElementId: string) {
function handleCloneElement(formElement: FormElementDto, position: number) { function handleCloneElement(formElement: FormElementDto, position: number) {
emit('clone:element', formElement, position) emit('clone:element', formElement, position)
} }
function applyCascadingEntryDelay(el: Element) {
const index = parseInt((el as HTMLElement).dataset.index ?? '0', 10)
;(el as HTMLElement).style.transitionDelay = `${index * 30}ms`
}
</script> </script>

View File

@@ -57,6 +57,7 @@
{{ currentFormElementSection.title }} {{ currentFormElementSection.title }}
</h1> </h1>
<TransitionGroup name="form-element" tag="div" class="relative">
<UCard <UCard
v-for="{ subsection, sectionIndex } in visibleSubsections" v-for="{ subsection, sectionIndex } in visibleSubsections"
:key="getSubsectionKey(currentFormElementSection, sectionIndex, subsection)" :key="getSubsectionKey(currentFormElementSection, sectionIndex, subsection)"
@@ -75,7 +76,10 @@
:all-form-elements="allFormElements" :all-form-elements="allFormElements"
@update:model-value=" @update:model-value="
(elements) => (elements) =>
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection)) handleFormElementUpdate(
elements,
getSubsectionKey(currentFormElementSection, sectionIndex, subsection)
)
" "
@add:input-form=" @add:input-form="
(position) => (position) =>
@@ -91,6 +95,7 @@
" "
/> />
</UCard> </UCard>
</TransitionGroup>
<UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6"> <UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6">
<div class="text-center py-8 text-dimmed"> <div class="text-center py-8 text-dimmed">