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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import packageInfo from '../../package.json';
|
|||||||
*/
|
*/
|
||||||
export type ChangelogEntry = {
|
export type ChangelogEntry = {
|
||||||
version: string;
|
version: string;
|
||||||
/** ISO date (YYYY-MM-DD) the version shipped. */a
|
/** ISO date (YYYY-MM-DD) the version shipped. */
|
||||||
date: string;
|
date: string;
|
||||||
highlights: string[];
|
highlights: string[];
|
||||||
};
|
};
|
||||||
@@ -17,6 +17,14 @@ export type ChangelogEntry = {
|
|||||||
export const APP_VERSION: string = packageInfo.version;
|
export const APP_VERSION: string = packageInfo.version;
|
||||||
|
|
||||||
export const changelog: ChangelogEntry[] = [
|
export const changelog: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.1.20',
|
||||||
|
date: '2026-06-13',
|
||||||
|
highlights: [
|
||||||
|
'App - Improvements',
|
||||||
|
'App - Bug fixes'
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '0.1.18',
|
version: '0.1.18',
|
||||||
date: '2026-06-12',
|
date: '2026-06-12',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
ThroughputProduct,
|
ThroughputProduct,
|
||||||
ThroughputQuantityType
|
ThroughputQuantityType
|
||||||
} from '$lib/types';
|
} 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 { fade } from 'svelte/transition';
|
||||||
import ThroughputProductPicker from '$lib/components/throughput/ThroughputProductPicker.svelte';
|
import ThroughputProductPicker from '$lib/components/throughput/ThroughputProductPicker.svelte';
|
||||||
import { formatDate as formatDisplayDate, formatLocaleNumber, toNum } from '$lib/format';
|
import { formatDate as formatDisplayDate, formatLocaleNumber, toNum } from '$lib/format';
|
||||||
@@ -105,6 +105,41 @@
|
|||||||
let composer = $state<HTMLElement | null>(null);
|
let composer = $state<HTMLElement | null>(null);
|
||||||
let deletingId = $state<number | 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
|
// 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).
|
// much goes to stock (the rest belongs to the order).
|
||||||
const isSplit = $derived(nForOrder && nForStock);
|
const isSplit = $derived(nForOrder && nForStock);
|
||||||
@@ -287,6 +322,7 @@
|
|||||||
upsertStatsEntry(created);
|
upsertStatsEntry(created);
|
||||||
page = 1;
|
page = 1;
|
||||||
highlightId = created.id;
|
highlightId = created.id;
|
||||||
|
flashSuccess();
|
||||||
}
|
}
|
||||||
const flashed = highlightId;
|
const flashed = highlightId;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -982,6 +1018,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
<style>
|
||||||
.throughput {
|
.throughput {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1961,6 +2015,83 @@
|
|||||||
to { opacity: 1; transform: none; }
|
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.
|
/* 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
|
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
|
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) {
|
@media (max-width: 1040px) {
|
||||||
.facts {
|
.facts {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -2130,8 +2279,12 @@
|
|||||||
.row.just-added {
|
.row.just-added {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
.modal-card {
|
.modal-card,
|
||||||
|
.success-card {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
.confetti {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user