refactor(ui): SaveFab shares ActionButton shell

ActionButton now renders as <a> (href) or <button> (onclick), so
SaveFab wraps it to inherit the shake/hover/focus behavior already
used by AddButton/EditButton. Body-parts review replaces its inline
save button with SaveFab for consistency.
This commit is contained in:
2026-04-21 08:43:23 +02:00
parent c45585baa5
commit d93006a319
4 changed files with 106 additions and 115 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.38.0",
"version": "1.38.1",
"private": true,
"type": "module",
"scripts": {
+97 -67
View File
@@ -1,84 +1,114 @@
<script lang='ts'>
import type { Snippet } from 'svelte';
let { href, ariaLabel = undefined, children } = $props<{ href: string, ariaLabel?: string, children?: Snippet }>();
import "$lib/css/action_button.css"
let {
href,
ariaLabel = undefined,
type = 'button',
onclick = undefined,
disabled = false,
children
} = $props<{
href?: string;
ariaLabel?: string;
type?: 'button' | 'submit' | 'reset';
onclick?: (e: MouseEvent) => void;
disabled?: boolean;
children?: Snippet;
}>();
import "$lib/css/action_button.css";
</script>
{#if href}
<a class="container action_button" {href} aria-label={ariaLabel}>
{@render children?.()}
</a>
{:else}
<button
class="container action_button"
{type}
{onclick}
{disabled}
aria-label={ariaLabel}
>
{@render children?.()}
</button>
{/if}
<style>
.container{
position: fixed;
bottom:0;
right:0;
width: 1rem;
height: 1rem;
padding: 2rem;
border-radius: var(--radius-pill);
margin: 2rem;
transition: var(--transition-normal);
background-color: var(--red);
display: grid;
justify-content: center;
align-content: center;
z-index: 100;
.container {
position: fixed;
bottom: 0;
right: 0;
width: 1rem;
height: 1rem;
padding: 2rem;
border-radius: var(--radius-pill);
margin: 2rem;
transition: var(--transition-normal);
background-color: var(--red);
display: grid;
justify-content: center;
align-content: center;
z-index: 100;
border: none;
cursor: pointer;
}
@media screen and (max-width: 500px) {
.container{
.container {
margin: 1rem;
}
}
:global(.icon_svg){
width: 2rem;
height: 2rem;
fill: white;
}
:root{
--angle: 15deg;
}
.container:hover,
.container:focus-within
{
background-color: var(--nord0);
box-shadow: 0em 0em 0.5em 0.5em rgba(0,0,0,0.2);
/*transform: scale(1.2,1.2);*/
animation: shake 0.5s;
animation-fill-mode: forwards;
}
:global(.container:hover .icon_svg),
:global(.container:focus-within .icon_svg){
:global(.icon_svg) {
width: 2rem;
height: 2rem;
fill: white;
}
@keyframes shake{
0%{
transform: rotate(0)
scale(1,1);
}
25%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle))
scale(1.2,1.2)
;
}
50%{
:root {
--angle: 15deg;
}
.container:hover,
.container:focus-within {
background-color: var(--nord0);
box-shadow: 0em 0em 0.5em 0.5em rgba(0, 0, 0, 0.2);
animation: shake 0.5s;
animation-fill-mode: forwards;
}
.container:disabled {
opacity: 0.5;
cursor: not-allowed;
animation: none;
}
.container:disabled:hover {
background-color: var(--red);
box-shadow: none;
animation: none;
}
:global(.container:hover .icon_svg),
:global(.container:focus-within .icon_svg) {
fill: white;
}
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(calc(-1* var(--angle)))
scale(1.2,1.2);
}
74%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle))
scale(1.2, 1.2);
@keyframes shake {
0% {
transform: rotate(0) scale(1, 1);
}
100%{
transform: rotate(0)
scale(1.2,1.2);
}
}
25% {
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle)) scale(1.2, 1.2);
}
50% {
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(calc(-1 * var(--angle))) scale(1.2, 1.2);
}
74% {
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle)) scale(1.2, 1.2);
}
100% {
transform: rotate(0) scale(1.2, 1.2);
}
}
</style>
<a class="container action_button" {href} aria-label={ariaLabel}>
{@render children?.()}
</a>
+3 -44
View File
@@ -1,51 +1,10 @@
<script>
import Check from '$lib/assets/icons/Check.svelte';
import ActionButton from './ActionButton.svelte';
let { disabled = false, onclick, label = 'Save', type = 'submit' } = $props();
</script>
<button
{type}
class="fab-save"
{onclick}
{disabled}
aria-label={label}
>
<ActionButton {type} {onclick} {disabled} ariaLabel={label}>
<Check fill="white" width="2rem" height="2rem" />
</button>
<style>
.fab-save {
position: fixed;
bottom: 0;
right: 0;
width: 1rem;
height: 1rem;
padding: 2rem;
margin: 2rem;
border: none;
border-radius: var(--radius-pill);
background-color: var(--red);
display: grid;
justify-content: center;
align-content: center;
cursor: pointer;
z-index: 100;
transition: background-color 0.2s;
}
.fab-save:hover, .fab-save:focus {
background-color: var(--nord11);
}
.fab-save:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media screen and (max-width: 500px) {
.fab-save {
margin: 1rem;
}
}
</style>
</ActionButton>
@@ -7,6 +7,7 @@
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
import DatePicker from '$lib/components/DatePicker.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
let { data } = $props();
@@ -460,9 +461,6 @@
<button type="button" class="ghost" onclick={() => { idx = 0; direction = -1; }}>
<ArrowLeft size={14} /> {t('edit_again', lang)}
</button>
<button type="button" class="nav-btn primary" onclick={save} disabled={saving}>
<Check size={16} /> {saving ? t('saving', lang) : t('save_measurement', lang)}
</button>
</div>
</section>
{/if}
@@ -579,6 +577,10 @@
<span class="bottom-spacer"></span>
{/if}
</footer>
{#if done}
<SaveFab type="button" onclick={save} disabled={saving} label={saving ? t('saving', lang) : t('save_measurement', lang)} />
{/if}
</div>
<style>