diff --git a/templates/react-javascript/babel.config.js b/templates/react-javascript/babel.config.cjs
similarity index 100%
rename from templates/react-javascript/babel.config.js
rename to templates/react-javascript/babel.config.cjs
diff --git a/templates/react-javascript/package.json b/templates/react-javascript/package.json
index 47739533..1885b473 100644
--- a/templates/react-javascript/package.json
+++ b/templates/react-javascript/package.json
@@ -1,5 +1,6 @@
{
"version": "0.11.1",
+ "type": "module",
"dependencies": {
"@nmfs-radfish/radfish": "^1.1.0",
"@nmfs-radfish/react-radfish": "^1.0.0",
diff --git a/templates/react-javascript/plugins/vite-plugin-radfish-theme.js b/templates/react-javascript/plugins/vite-plugin-radfish-theme.js
new file mode 100644
index 00000000..85b69a99
--- /dev/null
+++ b/templates/react-javascript/plugins/vite-plugin-radfish-theme.js
@@ -0,0 +1,216 @@
+/**
+ * RADFish Theme Vite Plugin
+ *
+ * This plugin connects radfish.config.js to your application:
+ * - Transforms index.html with config values (title, meta tags, favicon)
+ * - Exposes config to React via virtual module "virtual:radfish-config"
+ * - Provides helper to generate PWA manifest from config
+ */
+
+import fs from "fs";
+import path from "path";
+import { pathToFileURL } from "url";
+
+const VIRTUAL_MODULE_ID = "virtual:radfish-config";
+const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
+
+/**
+ * Default configuration values (used if radfish.config.js is missing)
+ */
+function getDefaultConfig() {
+ return {
+ app: {
+ name: "RADFish Application",
+ shortName: "RADFish",
+ description: "RADFish React App",
+ },
+ icons: {
+ logo: "/icons/radfish.png",
+ favicon: "/icons/radfish.ico",
+ appleTouchIcon: "/icons/radfish.png",
+ pwa: {
+ icon144: "/icons/144.png",
+ icon192: "/icons/192.png",
+ icon512: "/icons/512.png",
+ },
+ },
+ colors: {
+ primary: "#0054a4",
+ secondary: "#0093d0",
+ accent: "#00467f",
+ text: "#333",
+ error: "#af292e",
+ buttonHover: "#0073b6",
+ label: "#0054a4",
+ borderDark: "#565c65",
+ borderLight: "#ddd",
+ background: "#f4f4f4",
+ headerBackground: "#0054a4",
+ warningLight: "#fff3cd",
+ warningMedium: "#ffeeba",
+ warningDark: "#856404",
+ },
+ pwa: {
+ themeColor: "#0054a4",
+ backgroundColor: "#ffffff",
+ },
+ typography: {
+ fontFamily: "Arial Narrow, sans-serif",
+ },
+ };
+}
+
+/**
+ * Main Vite plugin for RADFish theming
+ */
+export function radFishThemePlugin() {
+ let config = null;
+ let resolvedViteConfig;
+
+ return {
+ name: "vite-plugin-radfish-theme",
+
+ // Store Vite config for later use
+ configResolved(viteConfig) {
+ resolvedViteConfig = viteConfig;
+ },
+
+ // Load radfish.config.js at build start
+ async buildStart() {
+ const configPath = path.resolve(
+ resolvedViteConfig.root,
+ "radfish.config.js",
+ );
+
+ if (fs.existsSync(configPath)) {
+ try {
+ // Use dynamic import with cache-busting for HMR
+ const configUrl = pathToFileURL(configPath).href;
+ const module = await import(`${configUrl}?update=${Date.now()}`);
+ config = module.default;
+ } catch (error) {
+ console.warn(
+ "[radfish-theme] Error loading radfish.config.js:",
+ error.message,
+ );
+ config = getDefaultConfig();
+ }
+ } else {
+ console.warn(
+ "[radfish-theme] No radfish.config.js found, using defaults",
+ );
+ config = getDefaultConfig();
+ }
+ },
+
+ // Handle virtual module imports
+ resolveId(id) {
+ if (id === VIRTUAL_MODULE_ID) {
+ return RESOLVED_VIRTUAL_MODULE_ID;
+ }
+ },
+
+ // Provide config as virtual module content
+ load(id) {
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) {
+ return `export default ${JSON.stringify(config)}`;
+ }
+ },
+
+ // Transform index.html with config values
+ transformIndexHtml(html) {
+ if (!config) return html;
+
+ return html
+ .replace(/
.*?<\/title>/, `${config.app.shortName}`)
+ .replace(
+ //,
+ ``,
+ )
+ .replace(
+ //,
+ ``,
+ )
+ .replace(
+ //,
+ ``,
+ )
+ .replace(
+ //,
+ ``,
+ );
+ },
+ };
+}
+
+/**
+ * Generate VitePWA manifest configuration from radfish.config.js
+ *
+ * Usage in vite.config.js:
+ * import radFishConfig from "./radfish.config.js";
+ * VitePWA({ manifest: getManifestFromConfig(radFishConfig) })
+ */
+export function getManifestFromConfig(config) {
+ return {
+ short_name: config.app.shortName,
+ name: config.app.name,
+ icons: [
+ {
+ src: "icons/radfish.ico",
+ sizes: "512x512 256x256 144x144 64x64 32x32 24x24 16x16",
+ type: "image/x-icon",
+ },
+ {
+ src: "icons/radfish-144.ico",
+ sizes: "144x144 64x64 32x32 24x24 16x16",
+ type: "image/x-icon",
+ },
+ {
+ src: "icons/radfish-144.ico",
+ type: "image/icon",
+ sizes: "144x144",
+ purpose: "any",
+ },
+ {
+ src: "icons/radfish-192.ico",
+ type: "image/icon",
+ sizes: "192x192",
+ purpose: "any",
+ },
+ {
+ src: "icons/radfish-512.ico",
+ type: "image/icon",
+ sizes: "512x512",
+ purpose: "any",
+ },
+ {
+ src: config.icons.pwa.icon144.replace(/^\//, ""),
+ type: "image/png",
+ sizes: "144x144",
+ purpose: "any",
+ },
+ {
+ src: config.icons.pwa.icon144.replace(/^\//, ""),
+ type: "image/png",
+ sizes: "144x144",
+ purpose: "maskable",
+ },
+ {
+ src: config.icons.pwa.icon192.replace(/^\//, ""),
+ type: "image/png",
+ sizes: "192x192",
+ purpose: "maskable",
+ },
+ {
+ src: config.icons.pwa.icon512.replace(/^\//, ""),
+ type: "image/png",
+ sizes: "512x512",
+ purpose: "maskable",
+ },
+ ],
+ start_url: ".",
+ display: "standalone",
+ theme_color: config.pwa.themeColor,
+ background_color: config.pwa.backgroundColor,
+ };
+}
diff --git a/templates/react-javascript/radfish.config.js b/templates/react-javascript/radfish.config.js
new file mode 100644
index 00000000..e87514ed
--- /dev/null
+++ b/templates/react-javascript/radfish.config.js
@@ -0,0 +1,62 @@
+/**
+ * RADFish Theme Configuration
+ *
+ * This file is the single source of truth for customizing your RADFish application's
+ * branding, colors, and appearance. Edit the values below to match your agency's style.
+ *
+ * After making changes, restart the development server to see updates.
+ */
+
+export default {
+ // Application Identity
+ app: {
+ name: "RADFish Application", // Full name shown in header
+ shortName: "RADFish", // Short name for browser tab and PWA
+ description: "RADFish React App", // Meta description for SEO
+ },
+
+ // Logo and Icon Paths (relative to public directory)
+ icons: {
+ logo: "/icons/radfish.png", // Main logo shown on home page
+ favicon: "/icons/radfish.ico", // Browser tab icon
+ appleTouchIcon: "/icons/radfish.png", // iOS home screen icon
+ // PWA icons for installed app
+ pwa: {
+ icon144: "/icons/144.png",
+ icon192: "/icons/192.png",
+ icon512: "/icons/512.png",
+ },
+ },
+
+ // Color Theme
+ // These values generate CSS custom properties (e.g., --noaa-dark-blue)
+ colors: {
+ primary: "#0054a4", // Main brand color (header, buttons)
+ secondary: "#0093d0", // Secondary actions
+ accent: "#00467f", // Accent elements
+ text: "#333", // Body text color
+ error: "#af292e", // Error messages
+ buttonHover: "#0073b6", // Button hover state
+ label: "#0054a4", // Form labels
+ borderDark: "#565c65", // Dark borders
+ borderLight: "#ddd", // Light borders
+ background: "#f4f4f4", // Page background
+ headerBackground: "#0054a4", // Header background
+
+ // Warning/alert colors
+ warningLight: "#fff3cd",
+ warningMedium: "#ffeeba",
+ warningDark: "#856404",
+ },
+
+ // PWA (Progressive Web App) Configuration
+ pwa: {
+ themeColor: "#0054a4", // Browser chrome color on mobile
+ backgroundColor: "#ffffff", // Splash screen background
+ },
+
+ // Typography
+ typography: {
+ fontFamily: "Arial Narrow, sans-serif",
+ },
+};
diff --git a/templates/react-javascript/src/App.jsx b/templates/react-javascript/src/App.jsx
index d333f639..0baa79fb 100644
--- a/templates/react-javascript/src/App.jsx
+++ b/templates/react-javascript/src/App.jsx
@@ -9,11 +9,14 @@ import {
PrimaryNav,
Header,
} from "@trussworks/react-uswds";
+import { useRadFishConfig } from "./hooks/useRadFishConfig.jsx";
import HomePage from "./pages/Home";
function App({ application }) {
const [isExpanded, setExpanded] = useState(false);
+ const config = useRadFishConfig();
+
return (
@@ -28,7 +31,7 @@ function App({ application }) {
>
-
RADFish Application
+
{config.app.name}
setExpanded((prvExpanded) => !prvExpanded)}
label="Menu"
diff --git a/templates/react-javascript/src/hooks/useRadFishConfig.jsx b/templates/react-javascript/src/hooks/useRadFishConfig.jsx
new file mode 100644
index 00000000..e0059226
--- /dev/null
+++ b/templates/react-javascript/src/hooks/useRadFishConfig.jsx
@@ -0,0 +1,62 @@
+/**
+ * RADFish Configuration Hook
+ *
+ * Provides access to radfish.config.js values in React components.
+ *
+ * Usage:
+ * import { useRadFishConfig } from "./hooks/useRadFishConfig";
+ *
+ * function MyComponent() {
+ * const config = useRadFishConfig();
+ * return {config.app.name}
;
+ * }
+ */
+
+import { createContext, useContext } from "react";
+import config from "virtual:radfish-config";
+
+// Create context with config as default value
+const RadFishConfigContext = createContext(config);
+
+/**
+ * Provider component for RADFish configuration.
+ *
+ * Wrap your app with this to provide config to all child components.
+ * Optionally pass a custom config to override the default.
+ *
+ * Usage:
+ *
+ *
+ *
+ *
+ * // Or with custom config override:
+ *
+ *
+ *
+ */
+export function RadFishConfigProvider({ children, config: customConfig }) {
+ const value = customConfig || config;
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to access RADFish configuration in components.
+ *
+ * Returns the full config object with app, icons, colors, pwa, and typography.
+ *
+ * Usage:
+ * const config = useRadFishConfig();
+ * console.log(config.app.name); // "RADFish Application"
+ * console.log(config.colors.primary); // "#0054a4"
+ * console.log(config.icons.logo); // "/icons/radfish.png"
+ */
+export function useRadFishConfig() {
+ return useContext(RadFishConfigContext);
+}
+
+// Direct export for non-component usage (e.g., utility functions)
+export { config as radFishConfig };
diff --git a/templates/react-javascript/src/pages/Home.jsx b/templates/react-javascript/src/pages/Home.jsx
index 2a993a26..f8edf0f2 100644
--- a/templates/react-javascript/src/pages/Home.jsx
+++ b/templates/react-javascript/src/pages/Home.jsx
@@ -2,23 +2,30 @@ import "../index.css";
import React from "react";
import { Button } from "@trussworks/react-uswds";
import { Link } from "react-router-dom";
+import { useRadFishConfig } from "../hooks/useRadFishConfig.jsx";
function HomePage() {
+ const config = useRadFishConfig();
+
return (
-
-

-
- Edit src/App.js and save to reload.
-
-
-
-
-
-
-
+
+

+
+ Edit src/App.js and save to reload.
+
+
+
+
+
+
+
);
}
diff --git a/templates/react-javascript/src/styles/theme.css b/templates/react-javascript/src/styles/theme.css
index 92b1e2d3..2c832205 100644
--- a/templates/react-javascript/src/styles/theme.css
+++ b/templates/react-javascript/src/styles/theme.css
@@ -1,30 +1,47 @@
+/*
+ * RADFish Theme Styles
+ *
+ * These are default/fallback values. To customize your theme,
+ * edit radfish.config.js in the project root instead of this file.
+ *
+ * The Vite plugin will override these variables based on your config.
+ */
+
:root {
+ /* Primary colors - customize via radfish.config.js colors.primary/secondary */
--noaa-dark-blue: #0054a4;
--noaa-light-blue: #0093d0;
+ --noaa-accent-color: #00467f;
+
+ /* Warning/alert colors - customize via radfish.config.js colors.warningLight/Medium/Dark */
--noaa-yellow-one: #fff3cd;
--noaa-yellow-two: #ffeeba;
--noaa-yellow-three: #856404;
- --noaa-accent-color: #00467f;
+
+ /* UI colors - customize via radfish.config.js colors section */
--noaa-text-color: #333;
--noaa-error-color: #af292e;
--noaa-button-hover: #0073b6;
--noaa-label-color: #0054a4;
--noaa-border-dark: #565c65;
--noaa-border-light: #ddd;
+
+ /* RADFish-specific variables (mapped from config) */
+ --radfish-background: #f4f4f4;
+ --radfish-header-bg: var(--noaa-dark-blue);
+ --radfish-font-family: Arial Narrow, sans-serif;
}
body {
- font-family:
- Arial Narrow,
- sans-serif;
+ font-family: var(--radfish-font-family);
color: var(--noaa-text-color);
- background-color: #f4f4f4;
+ background-color: var(--radfish-background);
line-height: 1.6;
border-radius: 4px;
}
.header-container {
- background: var(--noaa-dark-blue);
+ background: var(--radfish-header-bg);
}
.header-title {
diff --git a/templates/react-javascript/vite.config.js b/templates/react-javascript/vite.config.js
index e5d836a4..74b39e15 100644
--- a/templates/react-javascript/vite.config.js
+++ b/templates/react-javascript/vite.config.js
@@ -1,10 +1,16 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
+import {
+ radFishThemePlugin,
+ getManifestFromConfig,
+} from "./plugins/vite-plugin-radfish-theme.js";
+import radFishConfig from "./radfish.config.js";
export default defineConfig((env) => ({
base: "/",
plugins: [
+ radFishThemePlugin(),
react(),
VitePWA({
devOptions: {
@@ -16,68 +22,7 @@ export default defineConfig((env) => ({
strategies: "injectManifest",
srcDir: "src",
filename: "service-worker.js",
- manifest: {
- short_name: "RADFish",
- name: "RADFish React Boilerplate",
- icons: [
- {
- src: "icons/radfish.ico",
- sizes: "512x512 256x256 144x144 64x64 32x32 24x24 16x16",
- type: "image/x-icon",
- },
- {
- src: "icons/radfish-144.ico",
- sizes: "144x144 64x64 32x32 24x24 16x16",
- type: "image/x-icon",
- },
- {
- src: "icons/radfish-144.ico",
- type: "image/icon",
- sizes: "144x144",
- purpose: "any",
- },
- {
- src: "icons/radfish-192.ico",
- type: "image/icon",
- sizes: "192x192",
- purpose: "any",
- },
- {
- src: "icons/radfish-512.ico",
- type: "image/icon",
- sizes: "512x512",
- purpose: "any",
- },
- {
- src: "icons/144.png",
- type: "image/png",
- sizes: "144x144",
- purpose: "any",
- },
- {
- src: "icons/144.png",
- type: "image/png",
- sizes: "144x144",
- purpose: "maskable",
- },
- {
- src: "icons/192.png",
- type: "image/png",
- sizes: "192x192",
- purpose: "maskable",
- },
- {
- src: "icons/512.png",
- type: "image/png",
- sizes: "512x512",
- purpose: "maskable",
- },
- ],
- start_url: ".",
- display: "standalone",
- theme_color: "#000000",
- background_color: "#ffffff",
- },
+ manifest: getManifestFromConfig(radFishConfig),
}),
],
server: {