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:
2026-06-13 10:25:52 +12:00
co-authored by Claude Opus 4.8
parent 2de82776cb
commit b1c0d3f3da
3 changed files with 165 additions and 4 deletions
+9 -1
View File
@@ -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',
+155 -2
View File
@@ -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>