Skip to content
Open
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
1 change: 1 addition & 0 deletions templates/react-javascript/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"version": "0.11.1",
"type": "module",
"dependencies": {
"@nmfs-radfish/radfish": "^1.1.0",
"@nmfs-radfish/react-radfish": "^1.0.0",
Expand Down
216 changes: 216 additions & 0 deletions templates/react-javascript/plugins/vite-plugin-radfish-theme.js
Original file line number Diff line number Diff line change
@@ -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>.*?<\/title>/, `<title>${config.app.shortName}</title>`)
.replace(
/<meta name="theme-color" content=".*?" \/>/,
`<meta name="theme-color" content="${config.pwa.themeColor}" />`,
)
.replace(
/<meta name="description" content=".*?" \/>/,
`<meta name="description" content="${config.app.description}" />`,
)
.replace(
/<link rel="icon" type="image\/x-icon" href=".*?" \/>/,
`<link rel="icon" type="image/x-icon" href="${config.icons.favicon}" />`,
)
.replace(
/<link rel="apple-touch-icon" href=".*?" \/>/,
`<link rel="apple-touch-icon" href="${config.icons.appleTouchIcon}" />`,
);
},
};
}

/**
* 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,
};
}
62 changes: 62 additions & 0 deletions templates/react-javascript/radfish.config.js
Original file line number Diff line number Diff line change
@@ -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",
},
};
5 changes: 4 additions & 1 deletion templates/react-javascript/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Application application={application}>
<a className="usa-skipnav" href="#main-content">
Expand All @@ -28,7 +31,7 @@ function App({ application }) {
>
<div className="usa-nav-container">
<div className="usa-navbar">
<Title className="header-title">RADFish Application</Title>
<Title className="header-title">{config.app.name}</Title>
<NavMenuButton
onClick={() => setExpanded((prvExpanded) => !prvExpanded)}
label="Menu"
Expand Down
62 changes: 62 additions & 0 deletions templates/react-javascript/src/hooks/useRadFishConfig.jsx
Original file line number Diff line number Diff line change
@@ -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 <h1>{config.app.name}</h1>;
* }
*/

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:
* <RadFishConfigProvider>
* <App />
* </RadFishConfigProvider>
*
* // Or with custom config override:
* <RadFishConfigProvider config={customConfig}>
* <App />
* </RadFishConfigProvider>
*/
export function RadFishConfigProvider({ children, config: customConfig }) {
const value = customConfig || config;
return (
<RadFishConfigContext.Provider value={value}>
{children}
</RadFishConfigContext.Provider>
);
}

/**
* 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 };
Loading