Login screen redesign

This commit is contained in:
2026-04-29 19:16:23 +12:00
parent 761ebb050d
commit 2bb51ad467
5 changed files with 486 additions and 58 deletions
+46 -21
View File
@@ -2,6 +2,7 @@
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import Lean101Logo from '$lib/components/Lean101Logo.svelte';
import { clientSession, hasModuleAccess, sessionHydrated } from '$lib/session'; import { clientSession, hasModuleAccess, sessionHydrated } from '$lib/session';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import packageInfo from '../../../package.json'; import packageInfo from '../../../package.json';
@@ -216,6 +217,12 @@
}); });
}); });
$effect(() => {
if ($sessionHydrated && !$clientSession && !isRootRoute) {
goto('/', { replaceState: true });
}
});
onMount(() => { onMount(() => {
syncViewport(); syncViewport();
@@ -254,6 +261,19 @@
<title>{pageTitle(page.url.pathname)} | Hunter Premium Produce</title> <title>{pageTitle(page.url.pathname)} | Hunter Premium Produce</title>
</svelte:head> </svelte:head>
{#if !$clientSession}
<div class="signed-out-shell">
{#if isRootRoute}
{@render children()}
{:else}
<section class="locked-card loading-card signed-out-card">
<p class="workspace-label">Checking Session</p>
<h2>Returning to the client login screen.</h2>
<p>Only authenticated client users can open workspace routes directly.</p>
</section>
{/if}
</div>
{:else}
<div class="app-shell"> <div class="app-shell">
{#if showBottomNav && navOpen} {#if showBottomNav && navOpen}
<button aria-label="Close navigation" class="nav-backdrop" type="button" onclick={() => (navOpen = false)}></button> <button aria-label="Close navigation" class="nav-backdrop" type="button" onclick={() => (navOpen = false)}></button>
@@ -263,8 +283,7 @@
<aside class="sidebar"> <aside class="sidebar">
<div class="brand-row"> <div class="brand-row">
<a class="brand" href="/"> <a class="brand" href="/">
<span class="brand-mark">HP</span> <Lean101Logo className="sidebar-logo" showTagline={false} />
<span>Hunter Premium Produce</span>
</a> </a>
</div> </div>
@@ -440,19 +459,12 @@
</header> </header>
<main class="content"> <main class="content">
{#if !isRootRoute && (!$sessionHydrated || isRestoringSession)} {#if !isRootRoute && isRestoringSession}
<section class="locked-card loading-card"> <section class="locked-card loading-card">
<p class="workspace-label">Checking Session</p> <p class="workspace-label">Checking Session</p>
<h2>Restoring your client workspace.</h2> <h2>Restoring your client workspace.</h2>
<p>Refreshing the current page with the saved browser session before deciding whether sign-in is required.</p> <p>Refreshing the current page with the saved browser session before deciding whether sign-in is required.</p>
</section> </section>
{:else if !isRootRoute && !$clientSession}
<section class="locked-card">
<p class="workspace-label">Client Sign-In Required</p>
<h2>Sign in on the Hunter Premium Produce home page to unlock workspace data.</h2>
<p>The client-facing routes stay empty until a valid client session is active.</p>
<a href="/">Return to sign-in</a>
</section>
{:else} {:else}
{@render children()} {@render children()}
{/if} {/if}
@@ -624,7 +636,9 @@
{/if} {/if}
{/if} {/if}
{#if paletteOpen} {/if}
{#if $clientSession && paletteOpen}
<div class="palette-overlay" role="presentation" onclick={() => (paletteOpen = false)}> <div class="palette-overlay" role="presentation" onclick={() => (paletteOpen = false)}>
<div <div
class="palette" class="palette"
@@ -720,6 +734,15 @@
min-height: 100vh; min-height: 100vh;
} }
.signed-out-shell {
min-height: 100vh;
padding: 1.5rem;
}
.signed-out-card {
margin: 0 auto;
}
.nav-backdrop { .nav-backdrop {
display: none; display: none;
} }
@@ -753,14 +776,10 @@
} }
.brand { .brand {
display: inline-flex; display: block;
align-items: center; width: 100%;
gap: 0.68rem;
font-size: 1.08rem;
font-weight: 700;
} }
.brand-mark,
.nav-icon { .nav-icon {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -773,10 +792,16 @@
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.brand-mark { .sidebar :global(.sidebar-logo) {
width: 1.9rem; width: min(100%, 12.2rem);
height: 1.9rem; }
border-radius: 0.68rem;
.sidebar :global(.sidebar-logo .logo-copy strong) {
font-size: 1.6rem;
}
.sidebar :global(.sidebar-logo .logo-mark) {
width: 2.6rem;
} }
.nav-toggle, .nav-toggle,
@@ -0,0 +1,73 @@
<script lang="ts">
let { className = '', showTagline = true } = $props<{
className?: string;
showTagline?: boolean;
}>();
</script>
<div class={`lean101-logo ${className}`.trim()}>
<span class="logo-mark" aria-hidden="true">
<svg viewBox="0 0 320 320" preserveAspectRatio="xMidYMid meet" role="img" focusable="false">
<path
d="M82 18c4 0 8 1 11 3l82 48c7 4 11 12 11 20v16c0 6-3 11-8 14-5 3-11 3-16 0l-35-21c-12-7-27-7-39 0l-8 5c-12 7-19 20-19 34v107c0 8-4 16-11 20l-2 1c-5 3-11 3-16 0-5-3-8-8-8-14V42c0-9 5-17 13-21l34-20c3-1 7-2 10-2Z"
fill="#1672c5"
/>
<path
d="M145 72c8-5 18-5 26 0l77 45c8 5 13 13 13 22v88c0 9-5 17-13 22l-77 45c-8 5-18 5-26 0l-77-45c-8-5-13-13-13-22v-88c0-9 5-17 13-22l77-45Zm4 29-49 29c-8 5-13 13-13 22v62c0 9 5 17 13 22l49 29c8 5 18 5 26 0l49-29c8-5 13-13 13-22v-62c0-9-5-17-13-22l-49-29c-8-5-18-5-26 0Z"
fill="#2e9df4"
/>
<path
d="M238 201c0-14-7-27-19-34l-8-5c-12-7-27-7-39 0l-35 21c-5 3-11 3-16 0-5-3-8-8-8-14v-16c0-8 4-16 11-20l82-48c3-2 7-3 11-3 3 0 7 1 10 2l34 20c8 4 13 12 13 21v209c0 6-3 11-8 14-5 3-11 3-16 0l-2-1c-7-4-11-12-11-20V201Z"
fill="#1672c5"
/>
</svg>
</span>
<span class="logo-copy">
<strong>LEAN 101</strong>
{#if showTagline}
<small>SMARTER IMPROVEMENT SOLUTIONS</small>
{/if}
</span>
</div>
<style>
.lean101-logo {
display: inline-flex;
align-items: center;
gap: 0.7rem;
color: #0b76c8;
line-height: 1;
}
.logo-mark {
width: 3rem;
flex-shrink: 0;
line-height: 0;
}
.logo-mark svg {
display: block;
width: 100%;
height: auto;
}
.logo-copy {
display: grid;
gap: 0.14rem;
}
.logo-copy strong {
color: #0b76c8;
font-size: 1.95rem;
font-weight: 800;
letter-spacing: 0.03em;
}
.logo-copy small {
color: #6a7680;
font-size: 0.54rem;
font-weight: 700;
letter-spacing: 0.13em;
}
</style>
+351 -21
View File
@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api'; import { api } from '$lib/api';
import Lean101Logo from '$lib/components/Lean101Logo.svelte';
import { clientSession, sessionHydrated } from '$lib/session'; import { clientSession, sessionHydrated } from '$lib/session';
import type { Mix, ProductCostBreakdown, RawMaterial } from '$lib/types'; import type { Mix, ProductCostBreakdown, RawMaterial } from '$lib/types';
import packageInfo from '../../package.json';
type Segment = { type Segment = {
label: string; label: string;
@@ -30,6 +32,8 @@
let password = $state('changeme'); let password = $state('changeme');
let isLoggingIn = $state(false); let isLoggingIn = $state(false);
let loginError = $state(''); let loginError = $state('');
const currentYear = new Date().getFullYear();
const appVersion = `v${packageInfo.version}`;
const monthLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep']; const monthLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'];
@@ -252,41 +256,75 @@
</script> </script>
{#if !$sessionHydrated} {#if !$sessionHydrated}
<section class="dashboard-intro"> <section class="auth-stage auth-stage-loading">
<div> <div class="auth-card auth-card-loading">
<div class="auth-header">
<div class="client-logo-block">
<Lean101Logo className="hero-logo" />
<div class="client-logo-copy">
<p class="eyebrow">Client Workspace</p> <p class="eyebrow">Client Workspace</p>
<strong>Hunter Premium Produce</strong>
<span>Lean 101 client workspace access</span>
</div>
</div>
</div>
<div class="auth-copy">
<h2>Restoring your workspace.</h2> <h2>Restoring your workspace.</h2>
<p>Checking the saved client session before deciding whether sign-in is required.</p> <p>Checking the saved client session before deciding whether sign-in is required.</p>
</div> </div>
</section>
<section class="workspace-banner login-banner loading-banner"> <div class="auth-loading-panel">
<span class="loading-pulse" aria-hidden="true"></span>
<div> <div>
<p class="eyebrow">Checking Session</p> <strong>Checking Session</strong>
<h3>Hold while the app restores your client access state.</h3> <p>The sign-in form appears only when no valid local client session is available.</p>
<p>The sign-in form only appears if no valid local session is available.</p> </div>
</div>
<div class="auth-footer">
<div class="lean-brand">
<Lean101Logo className="footer-logo" showTagline={false} />
</div>
<div class="auth-meta">
<span>{appVersion}</span>
<span>&copy; {currentYear} Hunter Premium Produce</span>
</div>
</div>
</div> </div>
</section> </section>
{:else if !$clientSession} {:else if !$clientSession}
<section class="dashboard-intro"> <section class="auth-stage">
<div> <div class="auth-card auth-card-login">
<!-- <p class="eyebrow">Client Workspace</p>--> <div class="auth-header">
<h2>Welcome to the Hunter Premium Produce App</h2> <div class="client-logo-block">
<p>Sign in to load input pricing, Mix Master products, and Scenario Builder.</p> <Lean101Logo className="hero-logo" />
</div> <div class="client-logo-copy">
</section>
<section class="workspace-banner login-banner">
<div>
<p class="eyebrow">Client Sign-In</p> <p class="eyebrow">Client Sign-In</p>
<h3>Login to Hunter Premium Produce</h3> <strong>Hunter Premium Produce</strong>
<p>Enter your username & password to begin</p> <span>Lean 101 client workspace access</span>
</div>
</div>
<span class="auth-status-pill">Secure Workspace Access</span>
</div> </div>
<form class="signin-form" onsubmit={handleLogin}> <div class="auth-copy">
<h2>Welcome back.</h2>
<p>Sign in to load input pricing, Mix Master products, delivered product costing, and Scenario Builder.</p>
</div>
<form class="signin-form auth-form" onsubmit={handleLogin}>
<label class="field">
<span>Email</span>
<input bind:value={email} type="email" autocomplete="username" placeholder="Email" /> <input bind:value={email} type="email" autocomplete="username" placeholder="Email" />
</label>
<label class="field">
<span>Password</span>
<input bind:value={password} type="password" autocomplete="current-password" placeholder="Password" /> <input bind:value={password} type="password" autocomplete="current-password" placeholder="Password" />
<button class="primary-button" type="submit" disabled={isLoggingIn}> </label>
<button class="primary-button auth-submit" type="submit" disabled={isLoggingIn}>
{isLoggingIn ? 'Signing in...' : 'Sign In'} {isLoggingIn ? 'Signing in...' : 'Sign In'}
</button> </button>
</form> </form>
@@ -294,6 +332,17 @@
{#if loginError} {#if loginError}
<p class="login-error">{loginError}</p> <p class="login-error">{loginError}</p>
{/if} {/if}
<div class="auth-footer">
<div class="lean-brand">
<Lean101Logo className="footer-logo" showTagline={false} />
</div>
<div class="auth-meta">
<span>{appVersion}</span>
<span>&copy; {currentYear} Hunter Premium Produce</span>
</div>
</div>
</div>
</section> </section>
{:else} {:else}
<section class="dashboard-intro"> <section class="dashboard-intro">
@@ -601,6 +650,263 @@
text-transform: uppercase; text-transform: uppercase;
} }
.auth-stage {
min-height: calc(100vh - 3rem);
display: grid;
place-items: center;
padding: 1rem 0;
}
.auth-stage-loading {
align-items: center;
}
.auth-card {
position: relative;
width: min(100%, 38rem);
display: grid;
gap: 1.35rem;
padding: 1.5rem;
border: 1px solid rgba(212, 226, 218, 0.95);
border-radius: 1.7rem;
background:
radial-gradient(circle at top left, rgba(115, 197, 146, 0.16), transparent 32%),
radial-gradient(circle at bottom right, rgba(33, 94, 60, 0.1), transparent 30%),
rgba(255, 255, 255, 0.96);
box-shadow: 0 28px 70px rgba(17, 37, 25, 0.14);
backdrop-filter: blur(14px);
overflow: hidden;
}
.auth-card::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.42), transparent 35%),
linear-gradient(180deg, transparent, rgba(238, 248, 242, 0.55));
pointer-events: none;
}
.auth-card > * {
position: relative;
z-index: 1;
}
.auth-card-loading {
width: min(100%, 34rem);
}
.auth-card-login {
width: min(100%, 39rem);
}
.auth-header {
display: grid;
justify-items: center;
gap: 1rem;
text-align: center;
}
.client-logo-block {
display: grid;
justify-items: center;
gap: 1rem;
width: 100%;
min-width: 0;
}
.client-logo-copy {
display: grid;
gap: 0.28rem;
min-width: 0;
justify-items: center;
}
.client-logo-copy .eyebrow {
margin: 0;
}
.client-logo-copy strong {
font-size: 1.18rem;
}
.client-logo-copy span {
color: var(--muted);
font-size: 0.88rem;
}
:global(.hero-logo) {
width: min(100%, 19rem);
}
:global(.hero-logo .logo-mark) {
width: 4.8rem;
}
:global(.hero-logo .logo-copy strong) {
font-size: clamp(2rem, 4.6vw, 3.2rem);
}
:global(.hero-logo .logo-copy small) {
font-size: 0.64rem;
}
.auth-status-pill {
display: inline-flex;
align-items: center;
padding: 0.48rem 0.8rem;
border: 1px solid rgba(44, 123, 72, 0.12);
border-radius: 999px;
background: rgba(240, 249, 244, 0.96);
color: #1e6a3d;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
justify-self: center;
}
.auth-copy {
display: grid;
gap: 0.55rem;
}
.auth-copy h2 {
font-size: clamp(2.1rem, 4vw, 2.8rem);
line-height: 1.02;
}
.auth-copy p {
max-width: 32rem;
color: var(--muted);
font-size: 1rem;
line-height: 1.6;
}
.auth-loading-panel {
display: flex;
align-items: center;
gap: 0.95rem;
padding: 1rem 1.05rem;
border: 1px solid rgba(217, 228, 221, 0.92);
border-radius: 1.1rem;
background: rgba(248, 251, 249, 0.92);
}
.auth-loading-panel strong,
.auth-loading-panel p {
margin: 0;
}
.auth-loading-panel p {
margin-top: 0.18rem;
color: var(--muted);
}
.loading-pulse {
width: 0.95rem;
height: 0.95rem;
flex-shrink: 0;
border-radius: 999px;
background: linear-gradient(135deg, #2f7b48 0%, #174b2d 100%);
box-shadow: 0 0 0 0 rgba(47, 123, 72, 0.28);
animation: pulse 1.8s ease-out infinite;
}
.auth-form {
grid-template-columns: 1fr;
gap: 0.9rem;
width: 100%;
}
.field {
display: grid;
gap: 0.4rem;
}
.field span {
font-size: 0.84rem;
font-weight: 700;
color: #425248;
letter-spacing: 0.02em;
}
.auth-form input {
padding: 1rem 1.05rem;
border: 1px solid #d6e3db;
border-radius: 1rem;
background: rgba(248, 251, 249, 0.94);
transition:
border-color 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
}
.auth-form input:focus {
outline: none;
border-color: #4d9668;
box-shadow: 0 0 0 0.24rem rgba(77, 150, 104, 0.12);
background: #fff;
}
.auth-submit {
width: 100%;
min-height: 3.35rem;
margin-top: 0.2rem;
}
.auth-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-top: 0.15rem;
border-top: 1px solid rgba(217, 228, 221, 0.88);
}
.lean-brand {
display: inline-flex;
align-items: center;
}
:global(.footer-logo) {
width: 9.8rem;
}
:global(.footer-logo .logo-mark) {
width: 2rem;
}
:global(.footer-logo .logo-copy strong) {
font-size: 1.15rem;
}
.auth-meta {
display: grid;
justify-items: end;
gap: 0.12rem;
color: var(--muted);
font-size: 0.82rem;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(47, 123, 72, 0.26);
transform: scale(0.96);
}
70% {
box-shadow: 0 0 0 0.7rem rgba(47, 123, 72, 0);
transform: scale(1);
}
100% {
box-shadow: 0 0 0 0 rgba(47, 123, 72, 0);
transform: scale(0.96);
}
}
.dashboard-intro, .dashboard-intro,
.workspace-banner, .workspace-banner,
.dashboard-grid, .dashboard-grid,
@@ -1375,6 +1681,30 @@
} }
@media (max-width: 860px) { @media (max-width: 860px) {
.auth-stage {
min-height: auto;
padding: 0.4rem 0 0.8rem;
}
.auth-card {
padding: 1.15rem;
border-radius: 1.35rem;
}
.auth-header,
.auth-footer {
flex-direction: column;
align-items: flex-start;
}
.auth-copy h2 {
font-size: 1.9rem;
}
.auth-meta {
justify-items: start;
}
.dashboard-intro, .dashboard-intro,
.intro-actions, .intro-actions,
.workspace-banner, .workspace-banner,
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB