Release v0.1.23

This commit is contained in:
2026-06-15 11:59:04 +12:00
parent 250d6ab6a9
commit 8f9a7b8193
18 changed files with 3725 additions and 3298 deletions
@@ -0,0 +1,538 @@
<script lang="ts">
import { Plus, TriangleAlert, X } from 'lucide-svelte';
import ThroughputProductPicker from '$lib/components/throughput/ThroughputProductPicker.svelte';
import type { ThroughputProduct, ThroughputQuantityType } from '$lib/types';
let {
products,
editingId,
saving,
nDate = $bindable(''),
nProductId = $bindable(''),
nQuantity = $bindable(''),
nType = $bindable<ThroughputQuantityType>('bags'),
nBagSize = $bindable(''),
nStaff = $bindable(''),
nNotes = $bindable(''),
nForOrder = $bindable(false),
nForStock = $bindable(false),
nJobNumber = $bindable(''),
nStockQty = $bindable(''),
showNote = $bindable(false),
addError,
isSplit,
addTotalKg,
formatNumber,
onSubmit,
onDismissNote,
onCancelEdit,
composerRef = $bindable<HTMLElement | null>(null)
}: {
products: ThroughputProduct[];
editingId: number | null;
saving: boolean;
nDate?: string;
nProductId?: string;
nQuantity?: string;
nType?: ThroughputQuantityType;
nBagSize?: string;
nStaff?: string;
nNotes?: string;
nForOrder?: boolean;
nForStock?: boolean;
nJobNumber?: string;
nStockQty?: string;
showNote?: boolean;
addError: string;
isSplit: boolean;
addTotalKg: number | null;
formatNumber: (value: number | null | undefined, digits?: number) => string;
onSubmit: () => void;
onDismissNote: () => void;
onCancelEdit: () => void;
composerRef?: HTMLElement | null;
} = $props();
</script>
<div class="composer" class:editing={editingId != null} bind:this={composerRef}>
<div class="composer-head">
<div class="composer-title">
<h2>{editingId != null ? 'Edit packing run' : 'Add a packing run'}</h2>
</div>
{#if editingId != null}
<button type="button" class="cancel-edit" onclick={onCancelEdit}>
<X size={16} strokeWidth={2.4} /> Cancel edit
</button>
{/if}
</div>
<form class="add-row" onsubmit={(e) => { e.preventDefault(); onSubmit(); }}>
<div class="add-cell">
<span class="cell-label">Date</span>
<input type="date" bind:value={nDate} aria-label="Production date" />
</div>
<div class="add-cell add-product">
<span class="cell-label">Product</span>
<ThroughputProductPicker {products} bind:productId={nProductId} inputId="throughput-add-product" />
</div>
<div class="add-cell">
<span class="cell-label">Packed</span>
<div class="packed-inputs">
<input
class="qty"
type="number"
min="0"
step="0.01"
inputmode="decimal"
bind:value={nQuantity}
placeholder={nType === 'bags' ? 'Bags' : 'Total kg'}
aria-label={nType === 'bags' ? 'Number of bags' : 'Total kilograms'}
/>
<select class="unit" bind:value={nType} aria-label="Bags or kilograms">
<option value="bags">bags</option>
<option value="kg">kg (bulka)</option>
</select>
{#if nType === 'bags'}
<span class="times" aria-hidden="true">×</span>
<input
class="bag"
type="number"
min="0"
step="0.01"
inputmode="decimal"
bind:value={nBagSize}
placeholder="kg/bag"
aria-label="Kilograms per bag"
/>
{/if}
</div>
{#if nType === 'bags' && addTotalKg !== null}
<span class="packed-total">= {formatNumber(addTotalKg)} kg total</span>
{/if}
</div>
<div class="add-cell">
<span class="cell-label">Packed by</span>
<input type="text" bind:value={nStaff} placeholder="Name" aria-label="Packed by" />
</div>
<div class="add-cell add-dest">
<span class="cell-label">Destination</span>
<div class="dest-rows">
<div class="dest-line">
<label class="dest-toggle" class:on={nForOrder}>
<input type="checkbox" bind:checked={nForOrder} /> For an order
</label>
{#if nForOrder}
<input
class="dest-input"
type="text"
bind:value={nJobNumber}
placeholder="Job number (Order Circle)"
aria-label="Job number"
/>
{/if}
</div>
<div class="dest-line">
<label class="dest-toggle" class:on={nForStock}>
<input type="checkbox" bind:checked={nForStock} /> For stock
</label>
{#if isSplit}
<input
class="dest-input"
type="number"
min="0"
step="0.01"
inputmode="decimal"
bind:value={nStockQty}
placeholder={`To stock (${nType === 'bags' ? 'bags' : 'kg'})`}
aria-label="Amount going to stock"
/>
{/if}
</div>
</div>
</div>
<div class="add-cell add-action">
<button type="submit" class="add-entry-button" disabled={saving}>
<Plus size={18} strokeWidth={2.6} />
<span>{saving ? 'Saving…' : editingId != null ? 'Save' : 'Add'}</span>
</button>
</div>
<div class="add-extra">
{#if showNote}
<div class="note-field">
<input
class="note-input"
type="text"
bind:value={nNotes}
placeholder="Note (optional)"
aria-label="Note"
/>
<button
type="button"
class="note-dismiss"
title="Remove note"
aria-label="Remove note"
onclick={onDismissNote}
>
<X size={16} strokeWidth={2.4} />
</button>
</div>
{:else}
<button type="button" class="link-button" onclick={() => (showNote = true)}>+ Add a note</button>
{/if}
{#if addError}
<span class="add-error"><TriangleAlert size={15} strokeWidth={2.4} /> {addError}</span>
{/if}
</div>
</form>
</div>
<style>
.composer {
display: grid;
gap: 0.85rem;
border: 1px solid color-mix(in srgb, var(--color-brand) 16%, var(--color-border));
border-radius: 1rem;
background: var(--color-bg-surface);
box-shadow: 0 16px 34px -28px rgba(15, 23, 42, 0.28);
/* The product picker menu is absolutely positioned and needs to escape the
card bounds without being cut off. */
overflow: visible;
}
.composer-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.45rem 0;
}
.composer-title h2 {
margin: 0;
font-size: 1.22rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.add-row {
display: grid;
grid-template-columns: 7.5rem minmax(19rem, 1.7fr) minmax(14rem, 1.15fr) minmax(7rem, 0.65fr) minmax(14rem, 1.2fr) auto;
gap: 0.75rem 0.85rem;
align-items: start;
padding: 0 1.45rem 1.25rem;
}
.add-cell {
display: flex;
flex-direction: column;
gap: 0.3rem;
min-width: 0;
}
.add-row input:not([type='checkbox']),
.add-row select {
width: 100%;
min-height: 48px;
padding: 0.62rem 0.78rem;
border: 1px solid color-mix(in srgb, var(--color-brand) 14%, var(--color-border));
border-radius: 0.8rem;
font-size: 0.98rem;
color: var(--color-text-primary);
background: var(--color-bg-surface);
}
.add-row input:not([type='checkbox']):focus-visible,
.add-row select:focus-visible,
.cancel-edit:focus-visible,
.add-entry-button:focus-visible,
.note-dismiss:focus-visible {
outline: 3px solid var(--color-brand);
outline-offset: 2px;
}
.packed-inputs {
display: flex;
align-items: center;
gap: 0.4rem;
}
.packed-inputs .qty {
flex: 1 1 4.5rem;
min-width: 0;
}
.packed-inputs .unit {
flex: 0 0 4.7rem;
width: 4.7rem;
}
.packed-inputs .bag {
flex: 0 0 6rem;
width: 6rem;
}
.packed-inputs .times {
flex: 0 0 auto;
color: var(--color-text-secondary);
font-weight: 700;
}
.packed-total {
margin-top: 0.3rem;
font-size: 0.84rem;
font-weight: 650;
color: var(--color-success);
font-variant-numeric: tabular-nums;
}
.add-dest {
gap: 0.4rem;
}
.dest-rows {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.dest-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dest-line .dest-toggle {
flex: 0 0 auto;
min-width: 8.5rem;
}
.dest-line .dest-input {
flex: 1 1 auto;
min-width: 0;
width: auto;
}
.dest-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.68rem;
border: 1px solid color-mix(in srgb, var(--color-brand) 14%, var(--color-border));
border-radius: 0.72rem;
background: var(--color-bg-surface);
font-size: 0.88rem;
font-weight: 600;
color: var(--color-text-secondary);
cursor: pointer;
user-select: none;
}
.dest-toggle input {
width: 1.2rem;
height: 1.2rem;
min-height: 0;
margin: 0;
flex-shrink: 0;
accent-color: var(--color-brand);
cursor: pointer;
}
.dest-toggle.on {
border-color: var(--color-brand);
background: var(--color-brand-tint);
color: var(--color-success);
}
.dest-input {
width: 100%;
}
.add-action {
justify-content: center;
}
.add-entry-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 48px;
padding: 0.62rem 1.28rem;
background: var(--color-brand);
color: #fff;
border: 1px solid var(--color-brand);
border-radius: 0.8rem;
font-size: 0.98rem;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: background-color 160ms ease;
}
.add-entry-button:hover:not(:disabled) {
background: #126a33;
}
.add-entry-button:disabled {
opacity: 0.65;
cursor: progress;
}
.add-extra {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
padding-top: 0.15rem;
}
.link-button {
padding: 0.25rem 0;
background: none;
border: 0;
color: var(--color-success);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
}
.link-button:hover {
text-decoration: underline;
}
.note-field {
display: flex;
align-items: center;
gap: 0.4rem;
flex: 1 1 16rem;
max-width: 28rem;
}
.note-input {
flex: 1 1 auto;
min-width: 0;
}
.note-dismiss,
.cancel-edit {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border);
background: var(--color-bg-surface);
color: var(--color-text-secondary);
cursor: pointer;
}
.note-dismiss {
flex-shrink: 0;
width: 2.1rem;
height: 2.1rem;
padding: 0;
border-radius: 0.55rem;
transition: border-color 140ms ease, color 140ms ease, background-color 140ms ease;
}
.cancel-edit {
gap: 0.35rem;
padding: 0.5rem 0.85rem;
border-radius: 0.6rem;
font-size: 0.92rem;
font-weight: 600;
}
.note-dismiss:hover,
.cancel-edit:hover {
color: var(--color-text-primary);
border-color: var(--color-text-muted);
background: #fafbfc;
}
.add-error {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: #8a1622;
font-weight: 650;
font-size: 0.95rem;
}
.cell-label {
display: none;
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-muted);
}
.composer.editing {
border-color: var(--color-brand);
box-shadow: 0 0 0 1px var(--color-brand) inset;
}
@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) {
.add-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem 1rem;
}
.add-cell:nth-child(2),
.add-cell:nth-child(3),
.add-dest,
.add-action {
grid-column: 1 / -1;
}
.add-entry-button {
width: 100%;
}
}
@media (max-width: 760px) {
.composer-head {
padding-left: 1.15rem;
padding-right: 1.15rem;
}
.composer-title h2 {
font-size: 1.28rem;
}
.add-row {
grid-template-columns: 1fr 1fr;
gap: 0.85rem 1rem;
padding: 0 1.15rem 1.15rem;
}
.add-cell .cell-label {
display: block;
}
.add-cell:nth-child(2),
.add-dest,
.add-action {
grid-column: 1 / -1;
}
.add-entry-button {
width: 100%;
}
}
</style>