Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 7 additions & 165 deletions components/estimator/estimator-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<section className="relative cyber-card rounded-lg p-6 space-y-4">
<div
className={`absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent to-transparent ${
accent === "cyan"
? "via-neon-cyan"
: accent === "green"
? "via-neon-green"
: accent === "magenta"
? "via-neon-magenta"
: "via-neon-amber"
}`}
/>
<h3
className={`text-sm font-mono font-bold tracking-[0.2em] uppercase pb-2 border-b ${colorMap[accent]}`}
>
▸ {title}
</h3>
<div className="space-y-4">{children}</div>
</section>
);
}

function Field({
label,
suffix,
hint,
children,
}: {
label: string;
suffix?: string;
hint?: string;
children: React.ReactNode;
}) {
return (
<div>
<label className="flex items-center justify-between text-[10px] font-mono font-semibold text-text-secondary mb-1.5 tracking-wider uppercase">
<span>{label}</span>
{suffix && <span className="text-text-muted">{suffix}</span>}
</label>
{children}
{hint && (
<p className="text-[10px] font-mono text-text-muted mt-1">{hint}</p>
)}
</div>
);
}

function TextInput({
value,
onChange,
placeholder,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
}) {
return (
<input
type="text"
value={value}
onChange={(e) => 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 (
<input
type="number"
value={value}
onChange={(e) => {
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 (
<label className="flex items-center gap-3 p-3 rounded-lg border border-cyber-border bg-cyber-dark/50 hover:border-neon-cyan/30 cursor-pointer transition-all group">
<div className="relative">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="sr-only"
/>
<div
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all ${
checked
? "bg-neon-cyan/20 border-neon-cyan text-neon-cyan glow-cyan"
: "border-cyber-border group-hover:border-cyber-border-bright"
}`}
>
{checked && (
<svg
className="w-3 h-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
)}
</div>
</div>
<div className="flex-1">
<div className="text-sm text-text-secondary font-mono group-hover:text-text-primary transition-colors">
{label}
</div>
{sub && <div className="text-[10px] text-text-muted font-mono">{sub}</div>}
</div>
</label>
);
}
170 changes: 170 additions & 0 deletions components/estimator/form-primitives.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="relative cyber-card rounded-lg p-6 space-y-4">
<div
className={`absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent to-transparent ${
accent === "cyan"
? "via-neon-cyan"
: accent === "green"
? "via-neon-green"
: accent === "magenta"
? "via-neon-magenta"
: "via-neon-amber"
}`}
/>
<h3
className={`text-sm font-mono font-bold tracking-[0.2em] uppercase pb-2 border-b ${colorMap[accent]}`}
>
▸ {title}
</h3>
<div className="space-y-4">{children}</div>
</section>
);
}

export function Field({
label,
suffix,
hint,
children,
}: {
label: string;
suffix?: string;
hint?: string;
children: ReactNode;
}) {
return (
<div>
<label className="flex items-center justify-between text-[10px] font-mono font-semibold text-text-secondary mb-1.5 tracking-wider uppercase">
<span>{label}</span>
{suffix && <span className="text-text-muted">{suffix}</span>}
</label>
{children}
{hint && (
<p className="text-[10px] font-mono text-text-muted mt-1">{hint}</p>
)}
</div>
);
}

export function TextInput({
value,
onChange,
placeholder,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
}) {
return (
<input
type="text"
value={value}
onChange={(e) => 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 (
<input
type="number"
value={value}
onChange={(e) => {
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 (
<label className="flex items-center gap-3 p-3 rounded-lg border border-cyber-border bg-cyber-dark/50 hover:border-neon-cyan/30 cursor-pointer transition-all group">
<div className="relative">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="sr-only"
/>
<div
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all ${
checked
? "bg-neon-cyan/20 border-neon-cyan text-neon-cyan glow-cyan"
: "border-cyber-border group-hover:border-cyber-border-bright"
}`}
>
{checked && (
<svg
className="w-3 h-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
)}
</div>
</div>
<div className="flex-1">
<div className="text-sm text-text-secondary font-mono group-hover:text-text-primary transition-colors">
{label}
</div>
{sub && <div className="text-[10px] text-text-muted font-mono">{sub}</div>}
</div>
</label>
);
}
16 changes: 5 additions & 11 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;

Expand Down Expand Up @@ -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;
}

Expand Down
Loading