Gate B: SvelteKit capture→mask→numbers→review UI with deterministic pipeline
- Scaffold SvelteKit app (svelte.config.js, vite.config.ts, app.html, app.d.ts) - Build full capture flow: photo input → mask overlay + 4 knobs → axis overlay + rotation → length/thickness/SKU inputs → review + upload to /api/v1/slabs - Uses existing lib/pipeline/* pure modules (mask, axis, units, crop, pricing, types) - Uses existing validation/sku.ts for SKU validation - Svelte 5 runes ($state, $effect, $derived) for reactive state - Canvas-based rendering for mask overlay, axis visualization, and 3:4 crop preview - Dockerfile updated for Vite production build with static Node server on :80 - All 54 existing tests pass, build succeeds
This commit is contained in:
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for an explanation of these declarations
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SlabUploader</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="false">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
/** App state for the capture → mask → axis → numbers → review flow.
|
||||
* Uses Svelte 5 runes ($state, $derived) — no svelte/store dependency.
|
||||
*/
|
||||
import type {
|
||||
RgbaImage,
|
||||
MaskBuffer,
|
||||
MaskResult,
|
||||
KnobSettings,
|
||||
AxisSession,
|
||||
LengthAxis,
|
||||
WidthStats,
|
||||
CropResult,
|
||||
} from '$lib/pipeline/types.js';
|
||||
import type { SkuValidationResult } from '$lib/validation/sku.js';
|
||||
import { defaultKnobs } from '$lib/pipeline/mask.js';
|
||||
import { validateSku, isSkuValid } from '$lib/validation/sku.js';
|
||||
|
||||
/** Steps in the flow. */
|
||||
export type AppStep = 'capture' | 'mask' | 'axis' | 'numbers' | 'review';
|
||||
|
||||
export interface PhotoEntry {
|
||||
file: File;
|
||||
blob: Blob;
|
||||
rgba: RgbaImage | null;
|
||||
maskBuffer: MaskBuffer | null;
|
||||
axis: LengthAxis | null;
|
||||
cropResult: CropResult | null;
|
||||
widthStats: WidthStats | null;
|
||||
lengthIn: number | null;
|
||||
knobSettings: KnobSettings;
|
||||
maskResult: MaskResult | null;
|
||||
axisSession: AxisSession | null;
|
||||
}
|
||||
|
||||
export interface ReviewData {
|
||||
id: string;
|
||||
serverRev: number;
|
||||
wooProductId: number | null;
|
||||
wooAdminUrl: string | null;
|
||||
}
|
||||
|
||||
export interface SlabState {
|
||||
step: AppStep;
|
||||
photos: PhotoEntry[];
|
||||
currentPhotoIndex: number;
|
||||
lengthIn: number;
|
||||
thicknessIn: number;
|
||||
sku: string;
|
||||
skuValidation: SkuValidationResult;
|
||||
sqft: number;
|
||||
bdft: number;
|
||||
widthStats: WidthStats | null;
|
||||
cropResult: CropResult | null;
|
||||
meetsMinPx: boolean;
|
||||
uploaded: boolean;
|
||||
uploadError: string | null;
|
||||
uploading: boolean;
|
||||
reviewData: ReviewData | null;
|
||||
}
|
||||
|
||||
export function createSlabState(): SlabState {
|
||||
return {
|
||||
step: 'capture',
|
||||
photos: [],
|
||||
currentPhotoIndex: 0,
|
||||
lengthIn: 0,
|
||||
thicknessIn: 0,
|
||||
sku: '',
|
||||
skuValidation: { valid: false, errors: ['too_short'] },
|
||||
sqft: 0,
|
||||
bdft: 0,
|
||||
widthStats: null,
|
||||
cropResult: null,
|
||||
meetsMinPx: true,
|
||||
uploaded: false,
|
||||
uploadError: null,
|
||||
uploading: false,
|
||||
reviewData: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function setSku(sku: string): SkuValidationResult {
|
||||
return validateSku(sku);
|
||||
}
|
||||
|
||||
export function computeSqft(lengthIn: number, widthPx: number, scalePx: number): number {
|
||||
const widthIn = widthPx / scalePx;
|
||||
return (lengthIn * widthIn) / 144;
|
||||
}
|
||||
|
||||
export function computeBdft(sqft: number, thicknessIn: number): number {
|
||||
return sqft * thicknessIn;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types.js';
|
||||
export let data: LayoutData;
|
||||
</script>
|
||||
|
||||
<h1 class="app-header">SlabUploader</h1>
|
||||
|
||||
<slot />
|
||||
|
||||
<style>
|
||||
:global(*) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
:global(.app-wrapper) {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.app-header {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
|
||||
export function load() {
|
||||
return {};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user