Seed additional products, improve left rail, improve search box, add visuals to buttons, rename working documents to working docs and improve left rail nav

This commit is contained in:
2026-04-30 22:27:36 +12:00
parent 4f876372c2
commit 151676265c
10 changed files with 816 additions and 128 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "data-entry-app-frontend",
"version": "0.1.2",
"version": "0.1.3",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -3,10 +3,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+196 -59
View File
@@ -39,7 +39,7 @@
{ href: '/scenarios', label: 'Planning View', shortLabel: 'PV' }
];
const searchItems: SearchItem[] = [
const baseSearchItems: SearchItem[] = [
{
href: '/',
label: 'Open Dashboard',
@@ -104,10 +104,12 @@
let quickMenuOpen = $state(false);
let userMenuOpen = $state(false);
let navOpen = $state(false);
let workingDocumentsExpanded = $state(true);
let workingDocumentsExpanded = $state(false);
let showBottomNav = $state(false);
let isRestoringSession = $state(false);
let restoredToken = $state<string | null>(null);
let seededSearchItems = $state<SearchItem[]>([]);
let seededSearchToken = $state<string | null>(null);
let paletteInput: HTMLInputElement | null = $state(null);
const appVersion = `v${packageInfo.version}`;
const releaseStage = 'Alpha';
@@ -138,6 +140,10 @@
...visibleWorkingDocumentItems.slice(0, 2)
]
);
const workingDocumentsActive = $derived(
visibleWorkingDocumentItems.some((item) => matchesRoute(item.href, page.url.pathname))
);
const searchItems = $derived([...baseSearchItems, ...seededSearchItems]);
function matchesRoute(href: string, pathname: string) {
return href === '/' ? pathname === '/' : pathname.startsWith(href);
@@ -255,6 +261,61 @@
});
});
$effect(() => {
const hydrated = $sessionHydrated;
const session = $clientSession;
const token = session?.token ?? null;
if (!hydrated || !session || !token) {
seededSearchItems = [];
seededSearchToken = null;
return;
}
if (seededSearchToken === token) {
return;
}
seededSearchToken = token;
Promise.all([
hasModuleAccess(session, 'products') ? api.products() : Promise.resolve([]),
hasModuleAccess(session, 'mix_master') ? api.mixes() : Promise.resolve([]),
hasModuleAccess(session, 'mix_calculator') ? api.mixCalculatorSessions() : Promise.resolve([])
])
.then(([products, mixes, sessions]) => {
if (seededSearchToken !== token) {
return;
}
seededSearchItems = [
...products.map((product) => ({
href: '/products',
label: product.name,
description: `Product · ${product.client_name} · ${product.mix_name}`,
keywords: `product ${product.name} ${product.client_name} ${product.mix_name} ${product.unit_of_measure}`
})),
...mixes.map((mix) => ({
href: `/mixes/${mix.id}`,
label: mix.name,
description: `Mix · ${mix.client_name} · ${mix.total_mix_kg}kg`,
keywords: `mix ${mix.name} ${mix.client_name} ${mix.notes ?? ''} ${mix.ingredients.map((ingredient) => ingredient.raw_material_name).join(' ')}`
})),
...sessions.map((savedSession) => ({
href: `/mix-calculator/${savedSession.id}`,
label: `${savedSession.session_number} · ${savedSession.product_name}`,
description: `Mix Session · ${savedSession.prepared_by_name} · ${savedSession.mix_date}`,
keywords: `mix calculator session ${savedSession.session_number} ${savedSession.product_name} ${savedSession.mix_name} ${savedSession.client_name} ${savedSession.prepared_by_name} ${savedSession.notes ?? ''}`
}))
];
})
.catch(() => {
if (seededSearchToken === token) {
seededSearchItems = [];
}
});
});
$effect(() => {
if ($sessionHydrated && !$clientSession && !isRootRoute) {
goto('/', { replaceState: true });
@@ -370,11 +431,15 @@
<button
aria-controls="working-documents-nav"
aria-expanded={workingDocumentsExpanded}
class:active={workingDocumentsActive}
class="nav-group-toggle"
type="button"
onclick={() => (workingDocumentsExpanded = !workingDocumentsExpanded)}
>
<span class="nav-group-label">Working Documents</span>
<span class="nav-group-toggle-copy">
<span class="nav-icon muted">WD</span>
<span>Working Docs</span>
</span>
<span class:open={workingDocumentsExpanded} class="chevron"></span>
</button>
{#if workingDocumentsExpanded}
@@ -421,37 +486,12 @@
<div class="topbar-middle">
<button class="search-box topbar-search" type="button" aria-label="Search the workspace" onclick={() => openPalette()}>
<span class="search-icon"></span>
<span class="search-placeholder">Search pages, mixes, products, and settings...</span>
<span class="search-placeholder">Search products, mixes, sessions, and pages...</span>
<kbd>/</kbd>
</button>
</div>
<div class="topbar-actions">
<div class="menu-wrap">
<button
class="action-button"
type="button"
onclick={() => {
quickMenuOpen = !quickMenuOpen;
userMenuOpen = false;
}}
>
Quick Actions
<span class:open={quickMenuOpen} class="chevron"></span>
</button>
{#if quickMenuOpen}
<div class="menu-panel">
<a href="/mixes">Open mix costing</a>
<a href="/mixes/new">Create mix worksheet</a>
<a href="/mix-calculator">Open mix calculator</a>
<a href="/mix-calculator/new">Create mix session</a>
<a href="/products">Review delivered pricing</a>
<button type="button" onclick={() => openPalette('')}>Search the workspace</button>
</div>
{/if}
</div>
<div class="menu-wrap user-menu-wrap">
<button
aria-expanded={userMenuOpen}
@@ -465,7 +505,7 @@
<span class={`user-status-dot ${$clientSession ? 'live' : 'idle'}`}></span>
<span class="user-trigger-copy">
<span class="workspace-label">{$sessionHydrated ? ($clientSession ? 'Signed in' : 'Signed out') : 'Checking session'}</span>
<strong>{$sessionHydrated ? ($clientSession ? $clientSession.email : 'Sign in required') : 'Restoring workspace access'}</strong>
<strong>{$sessionHydrated ? ($clientSession ? $clientSession.name || 'Client account' : 'Sign in required') : 'Restoring workspace access'}</strong>
</span>
<span class:open={userMenuOpen} class="chevron"></span>
</button>
@@ -514,6 +554,33 @@
{/if}
</main>
</div>
<div class="quick-fab-wrap">
{#if quickMenuOpen}
<div class="menu-panel quick-fab-panel">
<a href="/mixes">Open mix costing</a>
<a href="/mixes/new">Create mix worksheet</a>
<a href="/mix-calculator">Open mix calculator</a>
<a href="/mix-calculator/new">Create mix session</a>
<a href="/products">Review delivered pricing</a>
<button type="button" onclick={() => openPalette('')}>Search the workspace</button>
</div>
{/if}
<button
aria-expanded={quickMenuOpen}
aria-label="Open quick access menu"
class="quick-fab"
type="button"
onclick={() => {
quickMenuOpen = !quickMenuOpen;
userMenuOpen = false;
}}
>
<span class={`quick-fab-plus ${quickMenuOpen ? 'open' : ''}`}></span>
<span>Quick Access</span>
</button>
</div>
</div>
{#if showBottomNav}
@@ -629,11 +696,15 @@
<button
aria-controls="drawer-working-documents-nav"
aria-expanded={workingDocumentsExpanded}
class:active={workingDocumentsActive}
class="nav-group-toggle drawer-group-toggle"
type="button"
onclick={() => (workingDocumentsExpanded = !workingDocumentsExpanded)}
>
<span class="drawer-group-label">Working Documents</span>
<span class="nav-group-toggle-copy">
<span class="nav-icon muted">WD</span>
<span>Working Documents</span>
</span>
<span class:open={workingDocumentsExpanded} class="chevron"></span>
</button>
{#if workingDocumentsExpanded}
@@ -710,7 +781,7 @@
>
<div class="palette-input-row">
<span class="search-icon"></span>
<input bind:this={paletteInput} bind:value={paletteQuery} placeholder="Search pages, workflows, and pricing views..." />
<input bind:this={paletteInput} bind:value={paletteQuery} placeholder="Search products, mixes, sessions, and pages..." />
<kbd>Esc</kbd>
</div>
@@ -785,7 +856,7 @@
.app-shell {
display: grid;
grid-template-columns: 228px minmax(0, 1fr);
grid-template-columns: 244px minmax(0, 1fr);
min-height: 100vh;
}
@@ -1003,25 +1074,31 @@
justify-content: space-between;
gap: 0.75rem;
width: 100%;
padding: 0 0.68rem;
padding: 0.72rem 0.68rem;
border: none;
border-radius: 0.82rem;
background: transparent;
color: inherit;
color: #304038;
cursor: pointer;
transition: background-color 160ms ease;
}
.nav-group-label,
.drawer-group-label {
margin: 0;
color: var(--muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
.nav-group-toggle:hover,
.nav-group-toggle.active {
background: var(--green-soft);
}
.nav-group-label {
padding: 0;
.nav-group-toggle.active {
color: var(--green-deep);
font-weight: 600;
}
.nav-group-toggle-copy {
display: inline-flex;
align-items: center;
gap: 0.68rem;
min-width: 0;
white-space: nowrap;
}
.nav-sublist {
@@ -1141,9 +1218,12 @@
.topbar-middle {
min-width: 0;
display: flex;
justify-content: center;
}
.topbar-search {
width: min(100%, 36rem);
min-height: 3rem;
background: #fff;
}
@@ -1186,15 +1266,6 @@
position: relative;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 0.62rem;
border-radius: 0.88rem;
padding: 0.68rem 0.84rem;
color: #304038;
}
.user-trigger {
min-width: 14rem;
display: inline-flex;
@@ -1291,6 +1362,71 @@
backdrop-filter: blur(10px);
}
.quick-fab-wrap {
position: fixed;
right: max(1rem, env(safe-area-inset-right));
bottom: max(1rem, env(safe-area-inset-bottom));
z-index: 46;
display: grid;
justify-items: end;
gap: 0.6rem;
}
.quick-fab {
display: inline-flex;
align-items: center;
gap: 0.72rem;
padding: 0.88rem 1.05rem;
border: none;
border-radius: 999px;
background: linear-gradient(135deg, #2f7b48 0%, #174b2d 100%);
color: #fff;
box-shadow: 0 18px 36px rgba(23, 75, 45, 0.26);
font-weight: 700;
letter-spacing: 0.01em;
cursor: pointer;
}
.quick-fab-panel {
position: static;
min-width: 15rem;
padding: 0.45rem;
border-radius: 1rem;
}
.quick-fab-plus {
position: relative;
width: 0.92rem;
height: 0.92rem;
flex-shrink: 0;
}
.quick-fab-plus::before,
.quick-fab-plus::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0.92rem;
height: 2px;
border-radius: 999px;
background: currentColor;
transform: translate(-50%, -50%);
transition: transform 140ms ease;
}
.quick-fab-plus::after {
transform: translate(-50%, -50%) rotate(90deg);
}
.quick-fab-plus.open::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.quick-fab-plus.open::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.menu-panel a,
.menu-panel button {
padding: 0.72rem 0.78rem;
@@ -1482,6 +1618,10 @@
backdrop-filter: blur(16px);
}
.quick-fab-wrap {
bottom: calc(max(0.8rem, env(safe-area-inset-bottom)) + 5.9rem);
}
.bottom-nav a,
.bottom-nav button {
min-width: 0;
@@ -1599,11 +1739,7 @@
}
.drawer-group-toggle {
padding: 0 0.2rem;
}
.drawer-group-label {
padding: 0;
padding-inline: 0.2rem;
}
.drawer-sublist {
@@ -1649,6 +1785,7 @@
.topbar-middle {
order: 3;
width: 100%;
}
.topbar-actions {
@@ -598,6 +598,11 @@
border: 1px solid var(--line-strong);
font-weight: 600;
cursor: pointer;
transition:
transform 140ms ease,
box-shadow 140ms ease,
background-color 140ms ease,
border-color 140ms ease;
}
.primary-button {
@@ -611,6 +616,22 @@
color: #304038;
}
.primary-button:hover:not(:disabled),
.secondary-button:hover:not(:disabled) {
transform: translateY(-1px);
}
.primary-button:hover:not(:disabled) {
box-shadow: 0 14px 28px rgba(23, 75, 45, 0.22);
filter: brightness(1.04);
}
.secondary-button:hover:not(:disabled) {
border-color: #9fb0a6;
background: #f6faf7;
box-shadow: 0 10px 22px rgba(24, 38, 29, 0.08);
}
button:disabled {
cursor: wait;
opacity: 0.7;
+17 -1
View File
@@ -100,6 +100,22 @@
}).format(new Date(value));
}
function greetingForAst() {
const astHour = Number(
new Intl.DateTimeFormat('en-AU', {
hour: 'numeric',
hour12: false,
timeZone: 'Australia/Brisbane'
}).format(new Date())
);
return astHour < 12 ? 'Good morning' : 'Good evening';
}
function firstName(name: string | null | undefined) {
return name?.trim().split(/\s+/)[0] ?? 'there';
}
function findHighestProduct(products: ProductCostBreakdown[]) {
return [...products].sort((left, right) => right.finished_product_delivered - left.finished_product_delivered)[0];
}
@@ -393,7 +409,7 @@
<p class="eyebrow">Client Workspace</p>
<span class="release-pill">{releaseStage}</span>
</div>
<h2>Hunter Premium Produce costing overview.</h2>
<h2>{greetingForAst()}, {firstName($clientSession?.name)}</h2>
<p>Track input pricing, mix performance, and delivered product outcomes from one client-facing workspace.</p>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB