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>/, `<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 ( -
- RADFish logo -

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

-

- - - -

-
+
+ {`${config.app.shortName} +

+ 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: {