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:
+9
-5
@@ -7,11 +7,15 @@ RUN npm ci --ignore-scripts
|
|||||||
|
|
||||||
COPY frontend/ .
|
COPY frontend/ .
|
||||||
|
|
||||||
# Build the SvelteKit app (stub for Gate B — will be updated when SvelteKit scaffolds)
|
RUN npm run build
|
||||||
# For now, copy a stub HTML page. Vite build runs in production mode later.
|
|
||||||
RUN mkdir -p build && \
|
|
||||||
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>SlabUploader</title></head><body><h1>SlabUploader — coming soon</h1><p>SvelteKit app stub. Update this Dockerfile when the SvelteKit app is scaffolded.</p></body></html>' > build/index.html
|
|
||||||
|
|
||||||
|
# adapter-static outputs to build/
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["node", "-e", "const h=require('http'),fs=require('fs'),url=require('url');h.createServer((req,res)=>{const f='build/index.html';res.writeHead(200,{'Content-Type':'text/html'}).end(fs.readFileSync(f))}).listen(80);console.log('frontend on :80')"]
|
CMD ["node", "-e", "\
|
||||||
|
const h=require('http'),fs=require('fs'),url=require('url'),path=require('path');\
|
||||||
|
const root='build';\
|
||||||
|
const types={'/':'application/json','js':'application/javascript','css':'text/css','html':'text/html','json':'application/json','svg':'image/svg+xml','png':'image/png','jpg':'image/jpeg','jpeg':'image/jpeg','ico':'image/x-icon','woff2':'font/woff2','woff':'font/woff','ttf':'font/ttf','otf':'font/otf','map':'application/json'};\
|
||||||
|
h.createServer((req,res)=>{const p=url.parse(req.url||'/').pathname||'/';const fp=path.join(root,p==='/'?'/index.html':p);try{const s=fs.statSync(fp);if(s.isDirectory()){fs.readFile(path.join(fp,'index.html'),'utf8',(e,d)=>{res.writeHead(200,{'Content-Type':'text/html'});res.end(d||'<h1>OK</h1>')})}else{const ext=path.extname(fp).slice(1);res.writeHead(200,{'Content-Type':types[ext]||'application/octet-stream'});fs.createReadStream(fp).pipe(res)}}catch(e){res.writeHead(404).end('Not found')}}).listen(80);\
|
||||||
|
console.log('frontend static build on :80');\
|
||||||
|
"]
|
||||||
|
|||||||
Generated
+1640
-163
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,21 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
|
"@sveltejs/kit": "^2.16.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.1.1",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^6.0.7",
|
||||||
"vitest": "^2.1.4"
|
"vitest": "^2.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
@@ -0,0 +1,13 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter(),
|
||||||
|
paths: {
|
||||||
|
base: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -5,9 +5,15 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"types": ["vitest/globals"],
|
"types": ["vitest/globals"],
|
||||||
"lib": ["ES2022"]
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"paths": {
|
||||||
|
"$lib/*": ["src/lib/*"],
|
||||||
|
"$app/*": [".svelte-kit/types/$app/*"],
|
||||||
|
"$types": [".svelte-kit/types/*"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"baseUrl": "."
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.svelte"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user