v0.1.20 - Even composer fields on laptops & add-success modal
- Throughput: even three-across layout for the "Add a packing run" fields on laptop widths so placeholders stay readable in the narrow content band - Throughput: centred "Added" confirmation modal with in-card confetti on save (3s), composer clears for the next entry - Fix stray character in changelog.ts type comment Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import packageInfo from '../../package.json';
|
||||
*/
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
/** ISO date (YYYY-MM-DD) the version shipped. */a
|
||||
/** ISO date (YYYY-MM-DD) the version shipped. */
|
||||
date: string;
|
||||
highlights: string[];
|
||||
};
|
||||
@@ -17,6 +17,14 @@ export type ChangelogEntry = {
|
||||
export const APP_VERSION: string = packageInfo.version;
|
||||
|
||||
export const changelog: ChangelogEntry[] = [
|
||||
{
|
||||
version: '0.1.20',
|
||||
date: '2026-06-13',
|
||||
highlights: [
|
||||
'App - Improvements',
|
||||
'App - Bug fixes'
|
||||
]
|
||||
},
|
||||
{
|
||||
version: '0.1.18',
|
||||
date: '2026-06-12',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
ThroughputProduct,
|
||||
ThroughputQuantityType
|
||||
} from '$lib/types';
|
||||
import { ArrowUpDown, CalendarDays, CalendarRange, Carrot, ChevronLeft, ChevronRight, Gauge, History, Pencil, Plus, TrendingUp, Trash2, TriangleAlert, Search, Wheat, X } from 'lucide-svelte';
|
||||
import { ArrowUpDown, CalendarDays, CalendarRange, Carrot, CheckCircle2, ChevronLeft, ChevronRight, Gauge, History, Pencil, Plus, TrendingUp, Trash2, TriangleAlert, Search, Wheat, X } from 'lucide-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ThroughputProductPicker from '$lib/components/throughput/ThroughputProductPicker.svelte';
|
||||
import { formatDate as formatDisplayDate, formatLocaleNumber, toNum } from '$lib/format';
|
||||
@@ -105,6 +105,41 @@
|
||||
let composer = $state<HTMLElement | null>(null);
|
||||
let deletingId = $state<number | null>(null);
|
||||
|
||||
// A brief centred "Added" confirmation with a confetti burst after a run is
|
||||
// logged, so a fast operator gets unmistakable feedback even with their eyes
|
||||
// on the keyboard. Auto-dismisses; pointer-events stay off so it never blocks
|
||||
// the next entry.
|
||||
type ConfettiPiece = {
|
||||
left: number;
|
||||
delay: number;
|
||||
duration: number;
|
||||
color: string;
|
||||
rotate: number;
|
||||
drift: number;
|
||||
size: number;
|
||||
};
|
||||
const CONFETTI_COLORS = ['#16a34a', '#f59e0b', '#3b82f6', '#ec4899', '#8b5cf6', '#ef4444'];
|
||||
let showSuccess = $state(false);
|
||||
let confetti = $state<ConfettiPiece[]>([]);
|
||||
let successTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function flashSuccess() {
|
||||
confetti = Array.from({ length: 42 }, (_, i) => ({
|
||||
left: Math.random() * 100,
|
||||
delay: Math.random() * 1.2,
|
||||
duration: 1 + Math.random() * 0.8,
|
||||
color: CONFETTI_COLORS[i % CONFETTI_COLORS.length],
|
||||
rotate: 220 + Math.random() * 360,
|
||||
drift: (Math.random() - 0.5) * 60,
|
||||
size: 6 + Math.random() * 6
|
||||
}));
|
||||
showSuccess = true;
|
||||
if (successTimer) clearTimeout(successTimer);
|
||||
successTimer = setTimeout(() => {
|
||||
showSuccess = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// When both checkpoints are ticked the run is split, so we need to know how
|
||||
// much goes to stock (the rest belongs to the order).
|
||||
const isSplit = $derived(nForOrder && nForStock);
|
||||
@@ -287,6 +322,7 @@
|
||||
upsertStatsEntry(created);
|
||||
page = 1;
|
||||
highlightId = created.id;
|
||||
flashSuccess();
|
||||
}
|
||||
const flashed = highlightId;
|
||||
setTimeout(() => {
|
||||
@@ -982,6 +1018,24 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showSuccess}
|
||||
<div class="success-overlay" role="presentation" transition:fade={{ duration: 160 }}>
|
||||
<div class="success-card" role="status" aria-live="polite">
|
||||
<div class="confetti" aria-hidden="true">
|
||||
{#each confetti as piece, i (i)}
|
||||
<span
|
||||
class="confetti-piece"
|
||||
style="left:{piece.left}%; width:{piece.size}px; height:{piece.size}px; background:{piece.color}; animation-delay:{piece.delay}s; animation-duration:{piece.duration}s; --rot:{piece.rotate}deg; --drift:{piece.drift}px;"
|
||||
></span>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="success-icon"><CheckCircle2 size={44} strokeWidth={2.4} /></span>
|
||||
<p class="success-title">Added</p>
|
||||
<p class="success-text">Your packing run has been added to the log.</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.throughput {
|
||||
display: grid;
|
||||
@@ -1961,6 +2015,83 @@
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* Transient "Added" confirmation. Sits above everything, but pointer-events
|
||||
stay off so the operator can keep typing the next run straight away. */
|
||||
.success-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.success-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: min(30rem, 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 2.6rem 2.8rem 2.8rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1.25rem;
|
||||
background: var(--color-bg-surface);
|
||||
box-shadow: 0 28px 70px -18px rgba(0, 0, 0, 0.45);
|
||||
text-align: center;
|
||||
animation: success-pop 240ms cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||
}
|
||||
@keyframes success-pop {
|
||||
from { opacity: 0; transform: scale(0.82); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.success-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4.2rem;
|
||||
height: 4.2rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-brand-tint);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
.success-title {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.success-text {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
/* Confetti is clipped to the card (overflow: hidden above) so it rains
|
||||
inside the dialog rather than across the page. */
|
||||
.confetti {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.confetti-piece {
|
||||
position: absolute;
|
||||
top: -12%;
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
animation-name: confetti-fall;
|
||||
animation-timing-function: ease-in;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
@keyframes confetti-fall {
|
||||
0% { transform: translate(0, 0) rotate(0deg); opacity: 0; }
|
||||
12% { opacity: 1; }
|
||||
100% { transform: translate(var(--drift), 340px) rotate(var(--rot)); opacity: 0.9; }
|
||||
}
|
||||
|
||||
/* Laptop and up: flatten both card groups into a single even 5-across row.
|
||||
display:contents hoists each .fact out of its <dl> so they all become
|
||||
direct items of the .throughput-summary grid. The container query on each
|
||||
@@ -1990,6 +2121,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Laptops: the single-row composer crams five fields into a narrow content
|
||||
band (the sidebar eats the width), squashing inputs until the placeholders
|
||||
are unreadable. Drop to an even three-across grid so every field gets a
|
||||
full, equal column — Date / Product / Packed on the first row, Packed by /
|
||||
Destination / Add on the second. */
|
||||
@media (max-width: 1440px) {
|
||||
.add-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1rem 1rem;
|
||||
}
|
||||
.add-action {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.add-entry-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.facts {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -2130,8 +2279,12 @@
|
||||
.row.just-added {
|
||||
animation: none;
|
||||
}
|
||||
.modal-card {
|
||||
.modal-card,
|
||||
.success-card {
|
||||
animation: none;
|
||||
}
|
||||
.confetti {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user