diff --git a/components/estimator/estimator-form.tsx b/components/estimator/estimator-form.tsx index cc30327..25fca5c 100644 --- a/components/estimator/estimator-form.tsx +++ b/components/estimator/estimator-form.tsx @@ -4,6 +4,13 @@ // Copyright (C) 2026 Steel-Tech / StructuPath import { useEffect, useMemo } from "react"; import { STATE_REGISTRY } from "@/content/state-registry"; +import { + Section, + Field, + TextInput, + NumberInput, + CheckRow, +} from "./form-primitives"; import { CRANE_DAY_RATES, type CraneType, @@ -335,168 +342,3 @@ export function EstimatorForm({ input, onChange }: Props) { ); } -/* ═══════════ PRIMITIVES ═══════════ */ - -function Section({ - title, - accent, - children, -}: { - title: string; - accent: "cyan" | "green" | "magenta" | "amber"; - children: React.ReactNode; -}) { - const colorMap = { - cyan: "text-neon-cyan text-glow-cyan border-neon-cyan/30", - green: "text-neon-green text-glow-green border-neon-green/30", - magenta: "text-neon-magenta text-glow-magenta border-neon-magenta/30", - amber: "text-neon-amber text-glow-amber border-neon-amber/30", - }; - return ( -
-
-

- ▸ {title} -

-
{children}
-
- ); -} - -function Field({ - label, - suffix, - hint, - children, -}: { - label: string; - suffix?: string; - hint?: string; - children: React.ReactNode; -}) { - return ( -
- - {children} - {hint && ( -

{hint}

- )} -
- ); -} - -function TextInput({ - value, - onChange, - placeholder, -}: { - value: string; - onChange: (v: string) => void; - placeholder?: string; -}) { - return ( - onChange(e.target.value)} - placeholder={placeholder} - className="cyber-input" - /> - ); -} - -function NumberInput({ - value, - onChange, - min, - step, -}: { - value: number; - onChange: (v: number) => void; - min?: number; - step?: number; -}) { - return ( - { - const n = parseFloat(e.target.value); - onChange(Number.isFinite(n) ? n : 0); - }} - onFocus={(e) => e.target.select()} - min={min} - step={step} - className="cyber-input" - /> - ); -} - -function CheckRow({ - label, - sub, - checked, - onChange, -}: { - label: string; - sub?: string; - checked: boolean; - onChange: (v: boolean) => void; -}) { - return ( - - ); -} diff --git a/components/estimator/form-primitives.tsx b/components/estimator/form-primitives.tsx new file mode 100644 index 0000000..c906297 --- /dev/null +++ b/components/estimator/form-primitives.tsx @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Steel-Tech / StructuPath +import type { ReactNode } from "react"; + +/* Presentational primitives for the estimator form — pure, props-only. Split + * out of estimator-form.tsx so that file stays focused on state and logic. */ + +export function Section({ + title, + accent, + children, +}: { + title: string; + accent: "cyan" | "green" | "magenta" | "amber"; + children: ReactNode; +}) { + const colorMap = { + cyan: "text-neon-cyan text-glow-cyan border-neon-cyan/30", + green: "text-neon-green text-glow-green border-neon-green/30", + magenta: "text-neon-magenta text-glow-magenta border-neon-magenta/30", + amber: "text-neon-amber text-glow-amber border-neon-amber/30", + }; + return ( +
+
+

+ ▸ {title} +

+
{children}
+
+ ); +} + +export function Field({ + label, + suffix, + hint, + children, +}: { + label: string; + suffix?: string; + hint?: string; + children: ReactNode; +}) { + return ( +
+ + {children} + {hint && ( +

{hint}

+ )} +
+ ); +} + +export function TextInput({ + value, + onChange, + placeholder, +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; +}) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + className="cyber-input" + /> + ); +} + +export function NumberInput({ + value, + onChange, + min, + step, +}: { + value: number; + onChange: (v: number) => void; + min?: number; + step?: number; +}) { + return ( + { + const n = parseFloat(e.target.value); + onChange(Number.isFinite(n) ? n : 0); + }} + onFocus={(e) => e.target.select()} + min={min} + step={step} + className="cyber-input" + /> + ); +} + +export function CheckRow({ + label, + sub, + checked, + onChange, +}: { + label: string; + sub?: string; + checked: boolean; + onChange: (v: boolean) => void; +}) { + return ( + + ); +} diff --git a/public/sw.js b/public/sw.js index 45ca676..d31334d 100644 --- a/public/sw.js +++ b/public/sw.js @@ -7,7 +7,7 @@ * - Cache-first for static assets (_next/static, fonts, images) */ -const VERSION = "ironforge-v1"; +const VERSION = "ironforge-v2"; const STATIC_CACHE = `${VERSION}-static`; const RUNTIME_CACHE = `${VERSION}-runtime`; @@ -77,17 +77,11 @@ self.addEventListener("fetch", (event) => { const url = new URL(request.url); if (url.origin !== self.location.origin) return; - // API: network-first, no offline fallback (let the app surface errors) + // API: network-only. Never cache API responses — a stale cached answer from + // the AI endpoints (or regulatory state data) would be misleading. Offline + // simply fails and the app surfaces the error. if (isApiRequest(url)) { - event.respondWith( - fetch(request) - .then((response) => { - const copy = response.clone(); - caches.open(RUNTIME_CACHE).then((cache) => cache.put(request, copy)); - return response; - }) - .catch(() => caches.match(request)), - ); + event.respondWith(fetch(request)); return; }