diff --git a/app/compare/page.tsx b/app/compare/page.tsx
index eb53e37..1e303b6 100644
--- a/app/compare/page.tsx
+++ b/app/compare/page.tsx
@@ -10,7 +10,7 @@ import { IBeamIcon } from "@/components/ui/ibeam-icon";
import { StateSelector } from "@/components/compare/state-selector";
import { ComparisonTable } from "@/components/compare/comparison-table";
import { ComparisonSummary } from "@/components/compare/comparison-summary";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
const LS_KEY = "ironforge_compare_states";
@@ -23,7 +23,7 @@ function loadSelected(): string[] {
if (!Array.isArray(parsed)) return [];
return parsed
.filter((c): c is string => typeof c === "string" && /^[A-Z]{2}$/.test(c))
- .filter((c) => !!STATE_REGISTRY[c])
+ .filter((c) => !!STATE_INDEX[c])
.slice(0, 3);
} catch {
return [];
@@ -113,7 +113,7 @@ export default function ComparePage() {
Comparing:{" "}
{selected
- .map((c) => STATE_REGISTRY[c]?.name ?? c)
+ .map((c) => STATE_INDEX[c]?.name ?? c)
.join(" vs. ")}
)}
diff --git a/app/wizard/summary/page.tsx b/app/wizard/summary/page.tsx
index 182fe9b..66d5791 100644
--- a/app/wizard/summary/page.tsx
+++ b/app/wizard/summary/page.tsx
@@ -22,7 +22,7 @@ import { PHASE_DEFINITIONS, getPhaseContent } from "@/content/phases";
import type { StateCode } from "@/content/phases";
import type { UserState } from "@/lib/types/wizard";
import { DEFAULT_STATE } from "@/lib/types/wizard";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
import {
toProgressJson,
toProgressCsv,
@@ -177,7 +177,7 @@ export default function SummaryPage() {
if (!loaded || !userState.profile.state || !summary) return null;
const stateCode = userState.profile.state as StateCode;
- const stateData = STATE_REGISTRY[stateCode];
+ const stateData = STATE_INDEX[stateCode];
const buildExport = (): ProgressExport => ({
generatedAt: new Date().toISOString(),
diff --git a/components/capability/cap-statement-form.tsx b/components/capability/cap-statement-form.tsx
index 25de56b..12c8d73 100644
--- a/components/capability/cap-statement-form.tsx
+++ b/components/capability/cap-statement-form.tsx
@@ -10,7 +10,7 @@ import {
SUGGESTED_NAICS,
emptyPastProject,
} from "@/lib/types/cap-statement";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
import { Plus, Trash2, Upload, X } from "lucide-react";
interface Props {
@@ -74,7 +74,7 @@ export function CapStatementForm({ data, onChange }: Props) {
reader.readAsDataURL(file);
}
- const states = Object.values(STATE_REGISTRY).sort((a, b) =>
+ const states = Object.values(STATE_INDEX).sort((a, b) =>
a.name.localeCompare(b.name),
);
diff --git a/components/compare/state-selector.tsx b/components/compare/state-selector.tsx
index 9dc1309..f5a52a8 100644
--- a/components/compare/state-selector.tsx
+++ b/components/compare/state-selector.tsx
@@ -4,9 +4,9 @@
// Copyright (C) 2026 Steel-Tech / StructuPath
import { useMemo, useState } from "react";
import { ChevronDown, Plus, X, Search } from "lucide-react";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
-const ALL_STATES = Object.values(STATE_REGISTRY).sort((a, b) =>
+const ALL_STATES = Object.values(STATE_INDEX).sort((a, b) =>
a.name.localeCompare(b.name),
);
@@ -77,7 +77,7 @@ export function StateSelector({
{slots.map((code, idx) => {
- const state = code ? STATE_REGISTRY[code] : null;
+ const state = code ? STATE_INDEX[code] : null;
const isOpen = openSlot === idx;
const isAddSlot = !state;
const disabled = isAddSlot && idx > selected.length;
diff --git a/components/estimator/estimate-results.tsx b/components/estimator/estimate-results.tsx
index 1976aec..41a1e15 100644
--- a/components/estimator/estimate-results.tsx
+++ b/components/estimator/estimate-results.tsx
@@ -4,7 +4,7 @@
// Copyright (C) 2026 Steel-Tech / StructuPath
import { formatCurrency, formatCurrencyPrecise } from "@/lib/estimator/calculate";
import type { EstimateInput, EstimateResult } from "@/lib/types/estimator";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
interface Props {
input: EstimateInput;
@@ -14,7 +14,7 @@ interface Props {
export function EstimateResults({ input, result }: Props) {
const { labor, materials, equipment, overhead, summary } = result;
const stateName = input.state
- ? STATE_REGISTRY[input.state]?.name || input.state
+ ? STATE_INDEX[input.state]?.name || input.state
: "—";
// Slice percentages for the stacked bar.
diff --git a/components/gc-directory/gc-search-filters.tsx b/components/gc-directory/gc-search-filters.tsx
index 2010502..a299f8a 100644
--- a/components/gc-directory/gc-search-filters.tsx
+++ b/components/gc-directory/gc-search-filters.tsx
@@ -9,7 +9,7 @@ import {
type ProjectType,
type GcSize,
} from "@/content/gc-directory";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
export interface GcFilters {
search: string;
@@ -33,7 +33,7 @@ interface GcSearchFiltersProps {
resultCount: number;
}
-const STATES = Object.values(STATE_REGISTRY).sort((a, b) =>
+const STATES = Object.values(STATE_INDEX).sort((a, b) =>
a.name.localeCompare(b.name),
);
diff --git a/components/onboarding/onboarding-chat.tsx b/components/onboarding/onboarding-chat.tsx
index 40b5949..9f02243 100644
--- a/components/onboarding/onboarding-chat.tsx
+++ b/components/onboarding/onboarding-chat.tsx
@@ -10,7 +10,7 @@ import { MarkdownRenderer } from "@/components/ui/markdown-renderer";
import { MatrixRain } from "@/components/ui/matrix-rain";
import { TronGrid } from "@/components/ui/tron-grid";
import { DecryptedText } from "@/components/ui/decrypted-text";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
import { saveUserState } from "@/lib/store/user-profile";
import type { UserProfile } from "@/lib/types/wizard";
import { DEFAULT_PROFILE, DEFAULT_STATE } from "@/lib/types/wizard";
@@ -201,8 +201,8 @@ export function OnboardingChat({ onExit }: OnboardingChatProps) {
*/
function fallbackQuestionFor(nextStage: Stage, updated: UserProfile): string {
const stateName =
- updated.state && STATE_REGISTRY[updated.state]
- ? STATE_REGISTRY[updated.state].name
+ updated.state && STATE_INDEX[updated.state]
+ ? STATE_INDEX[updated.state].name
: "";
switch (nextStage) {
case "experience":
@@ -224,8 +224,8 @@ export function OnboardingChat({ onExit }: OnboardingChatProps) {
function buildSummary(p: UserProfile): string {
const stateName =
- p.state && STATE_REGISTRY[p.state]
- ? STATE_REGISTRY[p.state].name
+ p.state && STATE_INDEX[p.state]
+ ? STATE_INDEX[p.state].name
: "—";
const lines = [
"**Here's your profile:**",
diff --git a/components/wizard/progress-sidebar.tsx b/components/wizard/progress-sidebar.tsx
index af72bed..cfc66be 100644
--- a/components/wizard/progress-sidebar.tsx
+++ b/components/wizard/progress-sidebar.tsx
@@ -5,7 +5,7 @@
import { PHASE_DEFINITIONS, getPhaseContent } from "@/content/phases";
import type { StateCode } from "@/content/phases";
import type { WizardProgress } from "@/lib/types/wizard";
-import { STATE_REGISTRY } from "@/content/state-registry";
+import { STATE_INDEX } from "@/content/state-index";
import Link from "next/link";
import { ChevronLeft, ChevronRight, Check, Circle, GitCompare } from "lucide-react";
import { IBeamIcon } from "@/components/ui/ibeam-icon";
@@ -177,8 +177,8 @@ export function ProgressSidebar({
- {STATE_REGISTRY[userState]
- ? `${STATE_REGISTRY[userState].emoji} ${STATE_REGISTRY[userState].name.toUpperCase()}`
+ {STATE_INDEX[userState]
+ ? `${STATE_INDEX[userState].emoji} ${STATE_INDEX[userState].name.toUpperCase()}`
: userState}
{
+ it("covers exactly the same state codes", () => {
+ expect(Object.keys(STATE_INDEX).sort()).toEqual(
+ Object.keys(STATE_REGISTRY).sort(),
+ );
+ });
+
+ it("matches code/name/emoji for every state", () => {
+ for (const [code, data] of Object.entries(STATE_REGISTRY)) {
+ expect(STATE_INDEX[code]).toEqual({
+ code: data.code,
+ name: data.name,
+ emoji: data.emoji,
+ });
+ }
+ });
+
+ it("STATE_LIST holds every state, sorted by name", () => {
+ expect(STATE_LIST).toHaveLength(Object.keys(STATE_REGISTRY).length);
+ const names = STATE_LIST.map((s) => s.name);
+ expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
+ });
+});
diff --git a/content/state-index.ts b/content/state-index.ts
new file mode 100644
index 0000000..56e847e
--- /dev/null
+++ b/content/state-index.ts
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright (C) 2026 Steel-Tech / StructuPath
+//
+// GENERATED from content/state-registry.ts — do not edit by hand.
+// A lightweight {code, name, emoji} index for dropdowns and name/emoji display.
+// Components that don't need full StateData import this instead of the registry,
+// keeping the ~26KB registry data out of their client bundles.
+// Regenerate: npx tsx scripts/gen-state-index.ts
+// content/state-index.test.ts asserts this stays in sync with the registry.
+
+export interface StateSummary {
+ code: string;
+ name: string;
+ emoji: string;
+}
+
+export const STATE_INDEX: Record = {
+ AK: { code: "AK", name: "Alaska", emoji: "🐻" },
+ AL: { code: "AL", name: "Alabama", emoji: "🏈" },
+ AR: { code: "AR", name: "Arkansas", emoji: "💎" },
+ AZ: { code: "AZ", name: "Arizona", emoji: "🌵" },
+ CA: { code: "CA", name: "California", emoji: "🌴" },
+ CO: { code: "CO", name: "Colorado", emoji: "⛰️" },
+ CT: { code: "CT", name: "Connecticut", emoji: "🏛️" },
+ DE: { code: "DE", name: "Delaware", emoji: "🦀" },
+ FL: { code: "FL", name: "Florida", emoji: "🌞" },
+ GA: { code: "GA", name: "Georgia", emoji: "🍑" },
+ HI: { code: "HI", name: "Hawaii", emoji: "🌺" },
+ IA: { code: "IA", name: "Iowa", emoji: "🌽" },
+ ID: { code: "ID", name: "Idaho", emoji: "🥔" },
+ IL: { code: "IL", name: "Illinois", emoji: "🏙️" },
+ IN: { code: "IN", name: "Indiana", emoji: "🏎️" },
+ KS: { code: "KS", name: "Kansas", emoji: "🌻" },
+ KY: { code: "KY", name: "Kentucky", emoji: "🐴" },
+ LA: { code: "LA", name: "Louisiana", emoji: "⚜️" },
+ MA: { code: "MA", name: "Massachusetts", emoji: "🎓" },
+ MD: { code: "MD", name: "Maryland", emoji: "🦀" },
+ ME: { code: "ME", name: "Maine", emoji: "🦞" },
+ MI: { code: "MI", name: "Michigan", emoji: "🏗️" },
+ MN: { code: "MN", name: "Minnesota", emoji: "❄️" },
+ MO: { code: "MO", name: "Missouri", emoji: "🏛️" },
+ MS: { code: "MS", name: "Mississippi", emoji: "🎸" },
+ MT: { code: "MT", name: "Montana", emoji: "🦌" },
+ NC: { code: "NC", name: "North Carolina", emoji: "🏔️" },
+ ND: { code: "ND", name: "North Dakota", emoji: "🦬" },
+ NE: { code: "NE", name: "Nebraska", emoji: "🌾" },
+ NH: { code: "NH", name: "New Hampshire", emoji: "🏔️" },
+ NJ: { code: "NJ", name: "New Jersey", emoji: "🏖️" },
+ NM: { code: "NM", name: "New Mexico", emoji: "🌶️" },
+ NV: { code: "NV", name: "Nevada", emoji: "🎰" },
+ NY: { code: "NY", name: "New York", emoji: "🗽" },
+ OH: { code: "OH", name: "Ohio", emoji: "🏈" },
+ OK: { code: "OK", name: "Oklahoma", emoji: "🌪️" },
+ OR: { code: "OR", name: "Oregon", emoji: "🌲" },
+ PA: { code: "PA", name: "Pennsylvania", emoji: "🔔" },
+ RI: { code: "RI", name: "Rhode Island", emoji: "⛵" },
+ SC: { code: "SC", name: "South Carolina", emoji: "🌴" },
+ SD: { code: "SD", name: "South Dakota", emoji: "🏔️" },
+ TN: { code: "TN", name: "Tennessee", emoji: "🎸" },
+ TX: { code: "TX", name: "Texas", emoji: "🤠" },
+ UT: { code: "UT", name: "Utah", emoji: "🏜️" },
+ VA: { code: "VA", name: "Virginia", emoji: "🏛️" },
+ VT: { code: "VT", name: "Vermont", emoji: "🍁" },
+ WA: { code: "WA", name: "Washington", emoji: "🌲" },
+ WI: { code: "WI", name: "Wisconsin", emoji: "🧀" },
+ WV: { code: "WV", name: "West Virginia", emoji: "⛏️" },
+ WY: { code: "WY", name: "Wyoming", emoji: "🤠" },
+};
+
+/** All states, sorted by name — for dropdowns. */
+export const STATE_LIST: StateSummary[] = Object.values(STATE_INDEX).sort(
+ (a, b) => a.name.localeCompare(b.name),
+);
diff --git a/scripts/gen-state-index.ts b/scripts/gen-state-index.ts
new file mode 100644
index 0000000..6d39570
--- /dev/null
+++ b/scripts/gen-state-index.ts
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright (C) 2026 Steel-Tech / StructuPath
+//
+// Generates content/state-index.ts — a standalone lightweight {code, name,
+// emoji} index — from the full STATE_REGISTRY. Run: npx tsx scripts/gen-state-index.ts
+import { writeFileSync } from "node:fs";
+import { STATE_REGISTRY } from "../content/state-registry";
+
+const entries = Object.values(STATE_REGISTRY)
+ .sort((a, b) => a.code.localeCompare(b.code))
+ .map(
+ (s) =>
+ ` ${s.code}: { code: ${JSON.stringify(s.code)}, name: ${JSON.stringify(
+ s.name,
+ )}, emoji: ${JSON.stringify(s.emoji)} },`,
+ )
+ .join("\n");
+
+const out = `// SPDX-License-Identifier: AGPL-3.0-or-later
+// Copyright (C) 2026 Steel-Tech / StructuPath
+//
+// GENERATED from content/state-registry.ts — do not edit by hand.
+// A lightweight {code, name, emoji} index for dropdowns and name/emoji display.
+// Components that don't need full StateData import this instead of the registry,
+// keeping the ~26KB registry data out of their client bundles.
+// Regenerate: npx tsx scripts/gen-state-index.ts
+// content/state-index.test.ts asserts this stays in sync with the registry.
+
+export interface StateSummary {
+ code: string;
+ name: string;
+ emoji: string;
+}
+
+export const STATE_INDEX: Record = {
+${entries}
+};
+
+/** All states, sorted by name — for dropdowns. */
+export const STATE_LIST: StateSummary[] = Object.values(STATE_INDEX).sort(
+ (a, b) => a.name.localeCompare(b.name),
+);
+`;
+
+writeFileSync(new URL("../content/state-index.ts", import.meta.url), out);
+console.log(
+ `Wrote content/state-index.ts with ${Object.keys(STATE_REGISTRY).length} states`,
+);