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/docs/theming.md b/templates/react-javascript/docs/theming.md
new file mode 100644
index 00000000..21f7ef16
--- /dev/null
+++ b/templates/react-javascript/docs/theming.md
@@ -0,0 +1,278 @@
+# RADFish Theming Guide
+
+This guide explains how to customize the look and feel of your RADFish application using USWDS, react-uswds, and RADFish theming.
+
+## Quick Start
+
+All theme customization happens in a single file:
+
+```
+themes/noaa-theme/styles/theme.scss
+```
+
+This file contains three sections:
+1. **USWDS Token Variables** - Design system colors (`$primary`, `$secondary`, etc.)
+2. **CSS Custom Properties** - Additional CSS variables (`:root { --noaa-* }`)
+3. **Component Overrides** - Custom styles for USWDS components
+
+## How It Works
+
+The RADFish theme plugin:
+
+1. **Parses `theme.scss`** and extracts SCSS `$variables` for USWDS configuration
+2. **Pre-compiles USWDS** with your color tokens
+3. **Compiles theme.scss** to CSS for custom properties and component overrides
+4. **Injects CSS** into your app via `` tags in `index.html`
+5. **Watches for changes** and auto-recompiles on save
+
+### Architecture
+
+```
+themes/noaa-theme/
+├── assets/ # Icons and logos
+│ ├── logo.png # Header logo
+│ ├── favicon.ico # Browser favicon
+│ ├── icon-144.png # PWA icon
+│ ├── icon-192.png # PWA icon
+│ └── icon-512.png # PWA icon
+└── styles/
+ └── theme.scss # All theme configuration (edit this)
+
+node_modules/.cache/radfish-theme/noaa-theme/ # Auto-generated (don't edit)
+├── _uswds-entry.scss # Generated USWDS config
+├── uswds-precompiled.css # Compiled USWDS styles
+├── theme.css # Compiled theme overrides
+└── .uswds-cache.json # Cache manifest
+```
+
+## Section 1: USWDS Token Variables
+
+At the top of `theme.scss`, define SCSS variables that configure the USWDS design system:
+
+```scss
+/* themes/noaa-theme/styles/theme.scss */
+
+// Primary colors
+$primary: #0055a4;
+$primary-dark: #00467f;
+$primary-light: #59b9e0;
+
+// Secondary colors
+$secondary: #007eb5;
+$secondary-dark: #006a99;
+
+// State colors
+$error: #d02c2f;
+$success: #4c9c2e;
+$warning: #ff8300;
+$info: #1ecad3;
+
+// Base/neutral colors
+$base-lightest: #ffffff;
+$base-lighter: #e8e8e8;
+$base: #71767a;
+$base-darkest: #333333;
+```
+
+### Available USWDS Tokens
+
+- **Base**: `base-lightest`, `base-lighter`, `base-light`, `base`, `base-dark`, `base-darker`, `base-darkest`
+- **Primary**: `primary-lighter`, `primary-light`, `primary`, `primary-vivid`, `primary-dark`, `primary-darker`
+- **Secondary**: `secondary-lighter`, `secondary-light`, `secondary`, `secondary-vivid`, `secondary-dark`, `secondary-darker`
+- **Accent Cool**: `accent-cool-lighter`, `accent-cool-light`, `accent-cool`, `accent-cool-dark`, `accent-cool-darker`
+- **Accent Warm**: `accent-warm-lighter`, `accent-warm-light`, `accent-warm`, `accent-warm-dark`, `accent-warm-darker`
+- **State**: `info`, `error`, `warning`, `success` (with `-lighter`, `-light`, `-dark`, `-darker` variants)
+- **Disabled**: `disabled-light`, `disabled`, `disabled-dark`
+
+See [USWDS Design Tokens](https://designsystem.digital.gov/design-tokens/color/theme-tokens/) for complete list.
+
+## Section 2: CSS Custom Properties
+
+Add custom CSS variables in the `:root` block for agency-specific colors:
+
+```scss
+/* themes/noaa-theme/styles/theme.scss */
+
+:root {
+ // Brand colors
+ --noaa-process-blue: #0093D0;
+ --noaa-reflex-blue: #0055A4;
+
+ // Regional colors
+ --noaa-region-alaska: #FF8300;
+ --noaa-region-west-coast: #4C9C2E;
+ --noaa-region-southeast: #B2292E;
+}
+```
+
+Use these in your application CSS:
+```css
+.region-badge--alaska {
+ background-color: var(--noaa-region-alaska);
+}
+```
+
+### Auto-Generated CSS Variables
+
+The plugin also auto-generates `--radfish-color-*` variables from your USWDS tokens:
+
+- `--radfish-color-primary`
+- `--radfish-color-secondary`
+- `--radfish-color-error`
+- `--radfish-color-success`
+- etc.
+
+## Section 3: Component Overrides
+
+At the bottom of `theme.scss`, add custom CSS for USWDS components:
+
+```scss
+/* themes/noaa-theme/styles/theme.scss */
+
+/* Header Background */
+.usa-header.header-container,
+header.usa-header {
+ background-color: var(--radfish-color-primary);
+}
+
+/* Custom button styles */
+.usa-button {
+ border-radius: 8px;
+ font-weight: 600;
+}
+
+/* Custom card styles */
+.usa-card {
+ border-color: #ddd;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+```
+
+### Available USWDS Components
+
+**Layout & Navigation**: `usa-header`, `usa-footer`, `usa-sidenav`, `usa-breadcrumb`, `usa-banner`
+
+**Forms & Inputs**: `usa-button`, `usa-input`, `usa-checkbox`, `usa-radio`, `usa-select`, `usa-form`
+
+**Content & Display**: `usa-card`, `usa-alert`, `usa-table`, `usa-list`, `usa-accordion`, `usa-tag`
+
+**Interactive**: `usa-modal`, `usa-tooltip`, `usa-pagination`
+
+## Developer Styles
+
+For application-specific styles (not theme-related), use:
+
+```
+src/styles/style.css
+```
+
+This file is loaded after theme styles, so you can override anything:
+
+```css
+/* src/styles/style.css */
+
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
+}
+
+.fish-data-card {
+ background: var(--radfish-color-base-lightest);
+ border: 1px solid #ddd;
+ padding: 1.5rem;
+}
+```
+
+## Configuration
+
+In `vite.config.js`, configure the app name and description:
+
+```javascript
+const configOverrides = {
+ app: {
+ name: "My App Name",
+ shortName: "MyApp",
+ description: "App description for PWA",
+ },
+};
+
+radFishThemePlugin("noaa-theme", configOverrides)
+```
+
+The theme name matches the folder in `themes/` directory.
+
+## Changing Assets
+
+Replace files in `themes/noaa-theme/assets/` to customize:
+
+| File | Purpose | Recommended Size |
+|------|---------|------------------|
+| `logo.png` | Header logo | Height ~48-72px |
+| `favicon.ico` | Browser tab icon | 64x64, 32x32, 16x16 |
+| `icon-144.png` | PWA icon | 144x144 |
+| `icon-192.png` | PWA icon | 192x192 |
+| `icon-512.png` | PWA icon/splash | 512x512 |
+
+## Creating a New Theme
+
+1. Create theme folder:
+ ```bash
+ mkdir -p themes/my-agency/assets themes/my-agency/styles
+ ```
+
+2. Add assets to `themes/my-agency/assets/`:
+ - `logo.png`, `favicon.ico`, `icon-144.png`, `icon-192.png`, `icon-512.png`
+
+3. Copy and customize the theme file:
+ ```bash
+ cp themes/noaa-theme/styles/theme.scss themes/my-agency/styles/
+ ```
+
+4. Update `vite.config.js`:
+ ```javascript
+ radFishThemePlugin("my-agency", {
+ app: { name: "My Agency App" }
+ })
+ ```
+
+5. Restart the dev server
+
+## CSS Load Order
+
+Styles are loaded in this order:
+
+1. **uswds-precompiled.css** - USWDS with your color tokens
+2. **theme.css** - Your CSS custom properties and component overrides
+3. **src/styles/style.css** - Your application styles
+
+This ensures correct CSS cascade: USWDS base → Theme overrides → App styles
+
+## Troubleshooting
+
+### Changes not appearing?
+
+- Save `theme.scss` - the dev server auto-restarts on changes
+- Clear browser cache: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
+- Restart dev server: `npm start`
+
+### Styles not applying?
+
+- Check CSS specificity - you may need more specific selectors
+- Inspect element in DevTools to see which styles are being applied
+- Ensure your selectors match USWDS class names exactly
+
+### Cache issues?
+
+Delete the cache folder and restart:
+```bash
+rm -rf node_modules/.cache/radfish-theme
+npm start
+```
+
+## Additional Resources
+
+- [USWDS Design System](https://designsystem.digital.gov/)
+- [USWDS Design Tokens](https://designsystem.digital.gov/design-tokens/)
+- [USWDS Components](https://designsystem.digital.gov/components/)
+- [React USWDS (Trussworks)](https://trussworks.github.io/react-uswds/)
diff --git a/templates/react-javascript/index.html b/templates/react-javascript/index.html
index 565b3820..b415e3fc 100644
--- a/templates/react-javascript/index.html
+++ b/templates/react-javascript/index.html
@@ -2,25 +2,18 @@
-
+
-
-
-
+
+
+
-
-
+
RADFish
diff --git a/templates/react-javascript/package-lock.json b/templates/react-javascript/package-lock.json
index 3ac49674..126640e7 100644
--- a/templates/react-javascript/package-lock.json
+++ b/templates/react-javascript/package-lock.json
@@ -34,6 +34,7 @@
"@lhci/cli": "^0.14.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "^16.0.1",
+ "@uswds/uswds": "^3.13.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.55.0",
"eslint-config-react-app": "^7.0.1",
@@ -43,6 +44,7 @@
"jsdom": "24.1.3",
"msw": "^2.6.5",
"prettier": "^3.1.0",
+ "sass": "^1.97.2",
"vite": "^5.1.5",
"vite-plugin-pwa": "0.21.0",
"vitest": "2.1.2"
@@ -2758,6 +2760,21 @@
"tree-kill": "^1.2.1"
}
},
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz",
+ "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
+ "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0"
+ }
+ },
"node_modules/@mswjs/interceptors": {
"version": "0.37.1",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.1.tgz",
@@ -2881,6 +2898,330 @@
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
},
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.4.tgz",
+ "integrity": "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.4",
+ "@parcel/watcher-darwin-arm64": "2.5.4",
+ "@parcel/watcher-darwin-x64": "2.5.4",
+ "@parcel/watcher-freebsd-x64": "2.5.4",
+ "@parcel/watcher-linux-arm-glibc": "2.5.4",
+ "@parcel/watcher-linux-arm-musl": "2.5.4",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.4",
+ "@parcel/watcher-linux-arm64-musl": "2.5.4",
+ "@parcel/watcher-linux-x64-glibc": "2.5.4",
+ "@parcel/watcher-linux-x64-musl": "2.5.4",
+ "@parcel/watcher-win32-arm64": "2.5.4",
+ "@parcel/watcher-win32-ia32": "2.5.4",
+ "@parcel/watcher-win32-x64": "2.5.4"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.4.tgz",
+ "integrity": "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.4.tgz",
+ "integrity": "sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.4.tgz",
+ "integrity": "sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.4.tgz",
+ "integrity": "sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.4.tgz",
+ "integrity": "sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.4.tgz",
+ "integrity": "sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.4.tgz",
+ "integrity": "sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.4.tgz",
+ "integrity": "sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.4.tgz",
+ "integrity": "sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz",
+ "integrity": "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.4.tgz",
+ "integrity": "sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.4.tgz",
+ "integrity": "sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz",
+ "integrity": "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@paulirish/trace_engine": {
"version": "0.0.23",
"resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.23.tgz",
@@ -3923,8 +4264,7 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "dev": true
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
@@ -4208,17 +4548,19 @@
"dev": true
},
"node_modules/@uswds/uswds": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.9.0.tgz",
- "integrity": "sha512-8THm36j7iLjrDiI1D0C6b3hHsmM/Sy5Iiz+IjE+i/gYzVUMG9XVthxAZYonhU97Q1b079n6nYwlUmDSYowJecQ==",
- "peer": true,
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/@uswds/uswds/-/uswds-3.13.0.tgz",
+ "integrity": "sha512-8P494gmXv/0sm09ExSdj8wAMjGLnM7UMRY/XgsMIRKnWfDXG+TyuCOKIuD4lqs+gLvSmi1nTQKyd0c0/A7VWJQ==",
+ "license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
- "object-assign": "4.1.1",
- "receptor": "1.0.0",
- "resolve-id-refs": "0.1.0"
+ "lit": "^3.2.1",
+ "receptor": "1.0.0"
},
"engines": {
"node": ">= 4"
+ },
+ "optionalDependencies": {
+ "sass-embedded-linux-x64": "^1.89.0"
}
},
"node_modules/@vitejs/plugin-react": {
@@ -5165,6 +5507,22 @@
"node": ">= 16"
}
},
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/chrome-launcher": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz",
@@ -5737,6 +6095,17 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/devtools-protocol": {
"version": "0.0.1312386",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
@@ -5821,7 +6190,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz",
"integrity": "sha512-QCqAWP3kwj8Gz9UXncVXQGdrhnWxD8SQBSeZp5pOsyCcQ6RpL738L1/tfuwBiMi6F1fYkxqPnBrFBR4L+f49Cg==",
- "peer": true,
"engines": {
"node": ">=4.0.0"
}
@@ -7703,6 +8071,13 @@
"integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==",
"dev": true
},
+ "node_modules/immutable": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
+ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -8610,8 +8985,7 @@
"node_modules/keyboardevent-key-polyfill": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keyboardevent-key-polyfill/-/keyboardevent-key-polyfill-1.1.0.tgz",
- "integrity": "sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ==",
- "peer": true
+ "integrity": "sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ=="
},
"node_modules/keyv": {
"version": "4.5.4",
@@ -8968,6 +9342,37 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
+ "node_modules/lit": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
+ "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^2.1.0",
+ "lit-element": "^4.2.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
+ "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0",
+ "@lit/reactive-element": "^2.1.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
+ "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -9084,8 +9489,7 @@
"node_modules/matches-selector": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/matches-selector/-/matches-selector-1.2.0.tgz",
- "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==",
- "peer": true
+ "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA=="
},
"node_modules/media-typer": {
"version": "0.3.0",
@@ -9496,6 +9900,14 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -10339,11 +10751,24 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/receptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/receptor/-/receptor-1.0.0.tgz",
"integrity": "sha512-yvVEqVQDNzEmGkluCkEdbKSXqZb3WGxotI/VukXIQ+4/BXEeXVjWtmC6jWaR1BIsmEAGYQy3OTaNgDj2Svr01w==",
- "peer": true,
"dependencies": {
"element-closest": "^2.0.1",
"keyboardevent-key-polyfill": "^1.0.2",
@@ -10515,12 +10940,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/resolve-id-refs": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/resolve-id-refs/-/resolve-id-refs-0.1.0.tgz",
- "integrity": "sha512-hNS03NEmVpJheF7yfyagNh57XuKc0z+NkSO0oBbeO67o6IJKoqlDfnNIxhjp7aTWwjmSWZQhtiGrOgZXVyM90w==",
- "peer": true
- },
"node_modules/restore-cursor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
@@ -10707,6 +11126,43 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "node_modules/sass": {
+ "version": "1.97.2",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz",
+ "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/sass-embedded-linux-x64": {
+ "version": "1.97.2",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.2.tgz",
+ "integrity": "sha512-bvAdZQsX3jDBv6m4emaU2OMTpN0KndzTAMgJZZrKUgiC0qxBmBqbJG06Oj/lOCoXGCxAvUOheVYpezRTF+Feog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
diff --git a/templates/react-javascript/package.json b/templates/react-javascript/package.json
index 47739533..b039f384 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",
@@ -59,6 +60,7 @@
"@lhci/cli": "^0.14.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "^16.0.1",
+ "@uswds/uswds": "^3.13.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.55.0",
"eslint-config-react-app": "^7.0.1",
@@ -68,6 +70,7 @@
"jsdom": "24.1.3",
"msw": "^2.6.5",
"prettier": "^3.1.0",
+ "sass": "^1.97.2",
"vite": "^5.1.5",
"vite-plugin-pwa": "0.21.0",
"vitest": "2.1.2"
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..4fad3ae5
--- /dev/null
+++ b/templates/react-javascript/plugins/vite-plugin-radfish-theme.js
@@ -0,0 +1,695 @@
+/**
+ * RADFish Theme Vite Plugin
+ *
+ * This plugin provides theming capabilities for RADFish applications:
+ * - Reads theme colors from SCSS files (themes//styles/theme.scss)
+ * - Exposes config values via import.meta.env.RADFISH_* constants
+ * - Injects CSS variables into HTML
+ * - Transforms index.html with config values (title, meta tags, favicon)
+ * - Writes manifest.json after build via closeBundle
+ *
+ * Usage:
+ * radFishThemePlugin("noaa-theme") // Use noaa-theme from themes/noaa-theme/
+ * radFishThemePlugin("noaa-theme", { app: {...} }) // With config overrides (non-color)
+ *
+ * Theme Structure:
+ * themes//
+ * assets/ - Theme icons (served in dev, copied on build)
+ * styles/theme.scss - Combined file with USWDS tokens, CSS variables, and component overrides
+ */
+
+import fs from "fs";
+import path from "path";
+import crypto from "crypto";
+import * as sass from "sass";
+
+/**
+ * Get the cache directory path for compiled theme files
+ * Uses node_modules/.cache/radfish-theme// to keep project clean
+ */
+function getCacheDir(themeName) {
+ return path.join(process.cwd(), "node_modules", ".cache", "radfish-theme", themeName);
+}
+
+/**
+ * Parse SCSS content string and extract variable definitions
+ * Supports simple variable definitions like: $variable-name: #hex;
+ * @param {string} content - SCSS content as a string
+ * @returns {Object} Object mapping variable names (without $) to values
+ */
+function parseScssContent(content) {
+ const variables = {};
+
+ // Match SCSS variable definitions: $variable-name: value;
+ // Captures: variable name (without $) and value (without semicolon)
+ const variableRegex = /^\s*\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/gm;
+
+ let match;
+ while ((match = variableRegex.exec(content)) !== null) {
+ const name = match[1].trim();
+ let value = match[2].trim();
+
+ // Remove !default flag if present
+ value = value.replace(/\s*!default\s*$/, "").trim();
+
+ // Convert kebab-case to camelCase for config compatibility
+ const camelName = name.replace(/-([a-z])/g, (_, letter) =>
+ letter.toUpperCase(),
+ );
+ variables[camelName] = value;
+ }
+
+ return variables;
+}
+
+/**
+ * Parse SCSS file and extract variable definitions
+ * Supports simple variable definitions like: $variable-name: #hex;
+ * @param {string} filePath - Path to the SCSS file
+ * @returns {Object} Object mapping variable names (without $) to values
+ */
+function parseScssVariables(filePath) {
+ if (!fs.existsSync(filePath)) {
+ return {};
+ }
+
+ const content = fs.readFileSync(filePath, "utf-8");
+ return parseScssContent(content);
+}
+
+
+/**
+ * Normalize color value (strip quotes if present)
+ */
+function normalizeColorValue(value) {
+ return value.replace(/['"]/g, '');
+}
+
+/**
+ * Check if a value is a USWDS system color token
+ * Matches patterns like: blue-60v, gray-cool-30, red-warm-50v, green-cool-40v
+ * See: https://designsystem.digital.gov/design-tokens/color/system-tokens/
+ */
+function isUswdsToken(value) {
+ // USWDS token pattern: color-family[-modifier]-grade[v]
+ // Examples: blue-60v, gray-cool-30, red-warm-50v, mint-cool-40v
+ const tokenPattern = /^(black|white|red|orange|gold|yellow|green|mint|cyan|blue|indigo|violet|magenta|gray)(-warm|-cool|-vivid)?(-[0-9]+v?)?$/;
+ return tokenPattern.test(value);
+}
+
+/**
+ * Format value for USWDS @use statement
+ * - USWDS tokens: quoted ('blue-60v')
+ * - Custom values (hex, etc.): unquoted (#0093D0)
+ */
+function formatUswdsValue(value) {
+ const normalized = normalizeColorValue(value);
+ if (isUswdsToken(normalized)) {
+ return `'${normalized}'`; // USWDS tokens are quoted
+ }
+ return normalized; // Custom values (hex, etc.) are unquoted
+}
+
+/**
+ * Compute MD5 hash of a file's content
+ */
+function computeFileHash(filePath) {
+ const content = fs.readFileSync(filePath, "utf-8");
+ return crypto.createHash("md5").update(content).digest("hex");
+}
+
+/**
+ * Check if USWDS needs recompilation based on cache
+ */
+function needsRecompilation(cacheDir, tokensPath) {
+ const cachePath = path.join(cacheDir, ".uswds-cache.json");
+ const cssPath = path.join(cacheDir, "uswds-precompiled.css");
+
+ // Need recompilation if cache or CSS doesn't exist
+ if (!fs.existsSync(cachePath) || !fs.existsSync(cssPath)) {
+ return true;
+ }
+
+ try {
+ const cache = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
+ return cache.tokensHash !== computeFileHash(tokensPath);
+ } catch {
+ return true;
+ }
+}
+
+/**
+ * Generate SCSS entry file content for USWDS compilation
+ */
+function generateUswdsEntryScss(uswdsTokens) {
+ // Build USWDS @use statement with all token variables
+ const uswdsTokensStr = Object.entries(uswdsTokens)
+ .map(([key, value]) => {
+ // Convert camelCase to kebab-case for USWDS format
+ const kebabKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
+ // Convert kebab-case to USWDS format: base-lightest → theme-color-base-lightest
+ const uswdsKey = `theme-color-${kebabKey}`;
+ // Format value: hex colors unquoted, token names quoted
+ const formattedValue = formatUswdsValue(value);
+ return ` $${uswdsKey}: ${formattedValue}`;
+ })
+ .join(",\n");
+
+ return `/**
+ * AUTO-GENERATED USWDS Entry Point
+ * Generated by RADFish theme plugin for sass.compile()
+ *
+ * DO NOT EDIT MANUALLY - changes will be overwritten
+ */
+
+@use "uswds-core" with (
+${uswdsTokensStr},
+ $theme-show-notifications: false
+);
+
+@forward "uswds";
+`;
+}
+
+/**
+ * Pre-compile USWDS with theme tokens to a static CSS file
+ */
+function precompileUswds(themeDir, themeName, uswdsTokens) {
+ const cacheDir = getCacheDir(themeName);
+ const entryPath = path.join(cacheDir, "_uswds-entry.scss");
+ const outputPath = path.join(cacheDir, "uswds-precompiled.css");
+ const tokensPath = path.join(themeDir, "styles", "theme.scss");
+
+ // Ensure cache directory exists
+ if (!fs.existsSync(cacheDir)) {
+ fs.mkdirSync(cacheDir, { recursive: true });
+ }
+
+ console.log("[radfish-theme] Pre-compiling USWDS...");
+ const startTime = Date.now();
+
+ // Generate entry SCSS with tokens
+ const entryContent = generateUswdsEntryScss(uswdsTokens);
+ fs.writeFileSync(entryPath, entryContent);
+
+ // Compile with sass
+ const result = sass.compile(entryPath, {
+ loadPaths: [path.join(process.cwd(), "node_modules/@uswds/uswds/packages")],
+ style: "compressed",
+ quietDeps: true,
+ });
+
+ fs.writeFileSync(outputPath, result.css);
+
+ // Save cache manifest
+ const cacheData = {
+ tokensHash: computeFileHash(tokensPath),
+ compiledAt: new Date().toISOString(),
+ };
+ fs.writeFileSync(
+ path.join(cacheDir, ".uswds-cache.json"),
+ JSON.stringify(cacheData, null, 2)
+ );
+
+ const elapsed = Date.now() - startTime;
+ console.log(`[radfish-theme] USWDS pre-compiled in ${elapsed}ms: ${outputPath}`);
+}
+
+/**
+ * Pre-compile theme SCSS file (theme.scss) to CSS
+ */
+function precompileThemeScss(themeDir, themeName) {
+ const cacheDir = getCacheDir(themeName);
+ const stylesDir = path.join(themeDir, "styles");
+
+ // Ensure cache directory exists
+ if (!fs.existsSync(cacheDir)) {
+ fs.mkdirSync(cacheDir, { recursive: true });
+ }
+
+ const inputPath = path.join(stylesDir, "theme.scss");
+ const outputPath = path.join(cacheDir, "theme.css");
+
+ if (fs.existsSync(inputPath)) {
+ try {
+ const result = sass.compile(inputPath, {
+ loadPaths: [
+ stylesDir,
+ cacheDir,
+ path.join(process.cwd(), "node_modules/@uswds/uswds/packages"),
+ ],
+ style: "compressed",
+ quietDeps: true,
+ });
+ fs.writeFileSync(outputPath, result.css);
+ console.log(`[radfish-theme] Compiled theme.scss -> theme.css`);
+ } catch (err) {
+ console.error(`[radfish-theme] Error compiling theme.scss:`, err.message);
+ }
+ }
+}
+
+
+/**
+ * Load theme tokens from theme.scss
+ * Returns: { uswdsTokens: {} }
+ */
+function loadThemeFiles(themeDir) {
+ const themeFile = path.join(themeDir, "styles", "theme.scss");
+
+ const uswdsTokens = fs.existsSync(themeFile)
+ ? parseScssVariables(themeFile)
+ : {};
+
+ return { uswdsTokens };
+}
+
+/**
+ * Copy directory recursively
+ */
+function copyDirSync(src, dest) {
+ if (!fs.existsSync(dest)) {
+ fs.mkdirSync(dest, { recursive: true });
+ }
+ const entries = fs.readdirSync(src, { withFileTypes: true });
+ for (const entry of entries) {
+ const srcPath = path.join(src, entry.name);
+ const destPath = path.join(dest, entry.name);
+ if (entry.isDirectory()) {
+ copyDirSync(srcPath, destPath);
+ } else {
+ fs.copyFileSync(srcPath, destPath);
+ }
+ }
+}
+
+/**
+ * Get content type for file extension
+ */
+function getContentType(ext) {
+ const types = {
+ ".png": "image/png",
+ ".ico": "image/x-icon",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".svg": "image/svg+xml",
+ ".webp": "image/webp",
+ };
+ return types[ext] || "application/octet-stream";
+}
+
+/**
+ * Generate manifest icon array for PWA manifest
+ * Uses generic filenames so developers can simply replace files in themes//assets/
+ */
+function getManifestIcons() {
+ return [
+ {
+ src: "icons/favicon.ico",
+ sizes: "64x64 32x32 24x24 16x16",
+ type: "image/x-icon",
+ },
+ {
+ src: "icons/icon-512.png",
+ type: "image/png",
+ sizes: "512x512",
+ purpose: "any",
+ },
+ {
+ src: "icons/icon-512.png",
+ type: "image/png",
+ sizes: "512x512",
+ purpose: "maskable",
+ },
+ ];
+}
+
+/**
+ * Default configuration values (used if radfish.config.js is missing)
+ * Exported so vite.config.js can import and use default colors
+ */
+export function getDefaultConfig() {
+ return {
+ app: {
+ name: "RADFish Application",
+ shortName: "RADFish",
+ description: "RADFish React App",
+ },
+ icons: {
+ logo: "/icons/logo.png",
+ favicon: "/icons/favicon.ico",
+ appleTouchIcon: "/icons/icon-512.png",
+ },
+ colors: {
+ primary: "#0054a4",
+ secondary: "#0093d0",
+ },
+ pwa: {
+ themeColor: "#0054a4",
+ backgroundColor: "#ffffff",
+ },
+ typography: {
+ fontFamily: "Arial Narrow, sans-serif",
+ },
+ };
+}
+
+/**
+ * Deep merge two objects (target values override source)
+ */
+function deepMerge(source, target) {
+ const result = { ...source };
+ for (const key of Object.keys(target)) {
+ if (target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
+ result[key] = deepMerge(source[key] || {}, target[key]);
+ } else {
+ result[key] = target[key];
+ }
+ }
+ return result;
+}
+
+/**
+ * Main Vite plugin for RADFish theming
+ * @param {string} themeName - Name of the theme folder in themes/ directory
+ * @param {Object} configOverrides - Optional config overrides (colors, app name, etc.)
+ */
+export function radFishThemePlugin(themeName = "noaa-theme", configOverrides = {}) {
+ let config = null;
+ let resolvedViteConfig = null;
+ let themeDir = null; // Path to themes// directory
+
+ return {
+ name: "vite-plugin-radfish-theme",
+
+ // Load config and return define values
+ async config(viteConfig) {
+ // Determine root directory
+ const root = viteConfig.root || process.cwd();
+
+ // Start with defaults, then merge provided overrides
+ config = deepMerge(getDefaultConfig(), configOverrides);
+
+ // Set theme directory based on theme name
+ const themeDirPath = path.resolve(root, "themes", themeName);
+ if (fs.existsSync(themeDirPath)) {
+ themeDir = themeDirPath;
+ console.log("[radfish-theme] Using theme:", themeName);
+
+ // Load theme tokens from theme.scss
+ const { uswdsTokens } = loadThemeFiles(themeDirPath);
+
+ if (Object.keys(uswdsTokens).length > 0) {
+ // Merge USWDS tokens into config colors for CSS variable injection
+ config.colors = deepMerge(config.colors, uswdsTokens);
+
+ // Auto-map PWA manifest colors from theme tokens
+ // Manifest theme color defaults to primary color from theme.scss
+ // Manifest background defaults to base-lightest from theme.scss
+
+ // Set manifest theme color (use primary token, fallback to default)
+ if (uswdsTokens.primary) {
+ // Primary is typically a token name like 'blue-60v' or hex like '#0054a4'
+ // For manifests we want hex, so if it looks like a token name, use our default
+ const primaryValue = normalizeColorValue(uswdsTokens.primary);
+ if (primaryValue.match(/^#/)) {
+ // It's already a hex color, use it directly
+ config.pwa.themeColor = primaryValue;
+ } else {
+ // It's a token name - use a safe default
+ // Most apps use blue for primary, so #0054a4 is a good default
+ config.pwa.themeColor = '#0054a4';
+ }
+ }
+
+ // Set manifest background color (use base-lightest token, fallback to white)
+ if (uswdsTokens.baseLight || uswdsTokens.baseLighter || uswdsTokens.baseLightest) {
+ // Try to find a light color, default to white
+ const bgValue = uswdsTokens.baseLightest || uswdsTokens.baseLighter || uswdsTokens.baseLight;
+ const normalizedBg = normalizeColorValue(bgValue);
+ if (normalizedBg.match(/^#/)) {
+ config.pwa.backgroundColor = normalizedBg;
+ } else {
+ // It's a token name, use white as safe default
+ config.pwa.backgroundColor = '#ffffff';
+ }
+ }
+
+ // Pre-compile USWDS to static CSS (with caching)
+ const tokensPath = path.join(themeDirPath, "styles", "theme.scss");
+ const cacheDir = getCacheDir(themeName);
+
+ if (needsRecompilation(cacheDir, tokensPath)) {
+ precompileUswds(themeDirPath, themeName, uswdsTokens);
+ } else {
+ console.log("[radfish-theme] Using cached USWDS compilation");
+ }
+
+ // Pre-compile theme SCSS file (theme.scss)
+ precompileThemeScss(themeDirPath, themeName);
+
+ console.log("[radfish-theme] Loaded theme from:", themeDirPath);
+ }
+ } else {
+ console.warn(`[radfish-theme] Theme "${themeName}" not found at ${themeDirPath}`);
+ }
+
+ // Return define values for import.meta.env.RADFISH_*
+ // These are available in app code via import.meta.env.RADFISH_.
+ // RADFISH_APP_NAME and RADFISH_LOGO are used by the default header component.
+ // The rest are available for developers to use in their own components, e.g.:
+ //
+ // ...
+ return {
+ define: {
+ "import.meta.env.RADFISH_APP_NAME": JSON.stringify(config.app.name),
+ "import.meta.env.RADFISH_SHORT_NAME": JSON.stringify(
+ config.app.shortName,
+ ),
+ "import.meta.env.RADFISH_DESCRIPTION": JSON.stringify(
+ config.app.description,
+ ),
+ "import.meta.env.RADFISH_LOGO": JSON.stringify(config.icons.logo),
+ "import.meta.env.RADFISH_FAVICON": JSON.stringify(
+ config.icons.favicon,
+ ),
+ "import.meta.env.RADFISH_PRIMARY_COLOR": JSON.stringify(
+ config.colors.primary,
+ ),
+ "import.meta.env.RADFISH_SECONDARY_COLOR": JSON.stringify(
+ config.colors.secondary,
+ ),
+ "import.meta.env.RADFISH_THEME_COLOR": JSON.stringify(
+ config.pwa.themeColor,
+ ),
+ "import.meta.env.RADFISH_BG_COLOR": JSON.stringify(
+ config.pwa.backgroundColor,
+ ),
+ },
+ };
+ },
+
+ // Store resolved config
+ configResolved(viteConfig) {
+ resolvedViteConfig = viteConfig;
+ },
+
+ // Serve theme assets in dev mode and watch SCSS for changes
+ configureServer(server) {
+ // Serve manifest.json in dev mode
+ server.middlewares.use("/manifest.json", (_req, res) => {
+ const manifest = {
+ short_name: config.app.shortName,
+ name: config.app.name,
+ description: config.app.description,
+ icons: getManifestIcons(),
+ start_url: ".",
+ display: "standalone",
+ theme_color: config.pwa.themeColor,
+ background_color: config.pwa.backgroundColor,
+ };
+ res.setHeader("Content-Type", "application/json");
+ res.end(JSON.stringify(manifest, null, 2));
+ });
+
+ // Serve theme assets if theme directory exists
+ if (!themeDir) return;
+
+ const cacheDir = getCacheDir(themeName);
+
+ // Serve pre-compiled CSS files at /radfish-theme/*
+ server.middlewares.use("/radfish-theme", (req, res, next) => {
+ const fileName = req.url?.replace(/^\//, "") || "";
+ const filePath = path.resolve(cacheDir, fileName);
+
+ // Prevent path traversal attacks
+ if (!filePath.startsWith(cacheDir)) {
+ return next();
+ }
+
+ if (fs.existsSync(filePath) && filePath.endsWith(".css")) {
+ res.setHeader("Content-Type", "text/css");
+ fs.createReadStream(filePath).pipe(res);
+ } else {
+ next();
+ }
+ });
+
+ // Watch theme SCSS file for changes and recompile
+ const themePath = path.join(themeDir, "styles", "theme.scss");
+
+ // Add theme file to watcher
+ if (fs.existsSync(themePath)) {
+ server.watcher.add(themePath);
+ }
+
+ server.watcher.on("change", (changedPath) => {
+ if (changedPath === themePath) {
+ // Theme file changed - recompile everything and restart
+ console.log("[radfish-theme] theme.scss changed, recompiling...");
+ const { uswdsTokens } = loadThemeFiles(themeDir);
+ precompileUswds(themeDir, themeName, uswdsTokens);
+ precompileThemeScss(themeDir, themeName);
+ console.log("[radfish-theme] Restarting server...");
+ server.restart();
+ }
+ });
+
+ const themeAssetsDir = path.join(themeDir, "assets");
+ if (!fs.existsSync(themeAssetsDir)) return;
+
+ // Serve /icons/* from themes//assets/ directory
+ server.middlewares.use("/icons", (req, res, next) => {
+ const filePath = path.resolve(themeAssetsDir, req.url?.replace(/^\//, "") || "");
+
+ // Prevent path traversal attacks
+ if (!filePath.startsWith(themeAssetsDir)) {
+ return next();
+ }
+
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
+ res.setHeader(
+ "Content-Type",
+ getContentType(path.extname(filePath)),
+ );
+ fs.createReadStream(filePath).pipe(res);
+ } else {
+ next();
+ }
+ });
+
+ console.log("[radfish-theme] Serving assets from:", themeAssetsDir);
+ },
+
+ // Transform index.html - inject CSS imports, variables, and update meta tags
+ transformIndexHtml(html) {
+ if (!config) return html;
+
+ // Generate CSS variables from all colors in config
+ // Convert camelCase keys to kebab-case for CSS variable names
+ const colorVariables = Object.entries(config.colors)
+ .map(([key, value]) => {
+ const kebabKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
+ return ` --radfish-color-${kebabKey}: ${value};`;
+ })
+ .join("\n");
+
+ // Inject theme CSS via link tags (all pre-compiled by plugin)
+ // This allows developers to not manually include CSS imports in their code
+ // Uses /radfish-theme/ path which is served by middleware in dev and copied to dist in build
+ const cssImports = `
+
+
+ `;
+
+ // Generate CSS variables from config
+ const cssVariables = `
+ `;
+
+ return html
+ .replace("", `${cssImports}\n${cssVariables}\n `)
+ .replace(
+ /.*?<\/title>/,
+ `${config.app.shortName}`,
+ )
+ .replace(
+ //,
+ ``,
+ )
+ .replace(
+ //,
+ ``,
+ )
+ .replace(
+ //,
+ ``,
+ )
+ .replace(
+ //,
+ ``,
+ );
+ },
+
+ // Write manifest.json after build completes
+ closeBundle() {
+ if (!config || !resolvedViteConfig) return;
+
+ // Only write manifest for build, not serve
+ const outDir = resolvedViteConfig.build?.outDir || "dist";
+ const outDirPath = path.resolve(resolvedViteConfig.root, outDir);
+ const manifestPath = path.resolve(outDirPath, "manifest.json");
+
+ // Ensure output directory exists
+ if (!fs.existsSync(outDirPath)) {
+ return; // Build hasn't created output dir yet
+ }
+
+ // Copy theme assets to dist/icons if using theme directory
+ if (themeDir) {
+ const themeAssetsDir = path.join(themeDir, "assets");
+ const distIconsDir = path.join(outDirPath, "icons");
+ if (fs.existsSync(themeAssetsDir)) {
+ copyDirSync(themeAssetsDir, distIconsDir);
+ console.log("[radfish-theme] Copied theme assets to:", distIconsDir);
+ }
+
+ // Copy pre-compiled CSS files to dist/radfish-theme/
+ const cacheDir = getCacheDir(themeName);
+ const distThemeDir = path.join(outDirPath, "radfish-theme");
+ if (fs.existsSync(cacheDir)) {
+ if (!fs.existsSync(distThemeDir)) {
+ fs.mkdirSync(distThemeDir, { recursive: true });
+ }
+ const cssFiles = ["uswds-precompiled.css", "theme.css"];
+ for (const cssFile of cssFiles) {
+ const srcPath = path.join(cacheDir, cssFile);
+ const destPath = path.join(distThemeDir, cssFile);
+ if (fs.existsSync(srcPath)) {
+ fs.copyFileSync(srcPath, destPath);
+ }
+ }
+ console.log("[radfish-theme] Copied CSS files to:", distThemeDir);
+ }
+ }
+
+ const manifest = {
+ short_name: config.app.shortName,
+ name: config.app.name,
+ description: config.app.description,
+ icons: getManifestIcons(),
+ start_url: ".",
+ display: "standalone",
+ theme_color: config.pwa.themeColor,
+ background_color: config.pwa.backgroundColor,
+ };
+
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
+ console.log("[radfish-theme] Wrote manifest.json to", manifestPath);
+ },
+ };
+}
diff --git a/templates/react-javascript/src/App.jsx b/templates/react-javascript/src/App.jsx
index d333f639..62a0a9d4 100644
--- a/templates/react-javascript/src/App.jsx
+++ b/templates/react-javascript/src/App.jsx
@@ -1,19 +1,91 @@
+/**
+ * App.jsx - Main Application Component
+ *
+ * Welcome to RADFish! This is the entry point for your application.
+ *
+ * This file sets up:
+ * - The Application wrapper (provides offline storage, state management)
+ * - Header with navigation
+ * - React Router for page routing
+ *
+ * Quick Start:
+ * 1. Add new pages in src/pages/
+ * 2. Add routes in the section below
+ * 3. Add navigation links in the ExtendedNav primaryItems array
+ *
+ * Theme customization:
+ * - Edit themes/noaa-theme/styles/theme.scss for colors and styles
+ * - App name and icons are configured in the theme plugin (vite.config.js)
+ *
+ * Learn more: https://nmfs-radfish.github.io/radfish/
+ */
+
import "./index.css";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
-import { useState } from "react";
+import React, { useState } from "react";
import { Application } from "@nmfs-radfish/react-radfish";
import {
GridContainer,
- Title,
NavMenuButton,
- PrimaryNav,
+ NavDropDownButton,
+ Menu,
+ ExtendedNav,
Header,
} from "@trussworks/react-uswds";
import HomePage from "./pages/Home";
+function onToggle(index, setIsOpen) {
+ setIsOpen((prev) => prev.map((val, i) => (i === index ? !val : false)));
+}
+
function App({ application }) {
const [isExpanded, setExpanded] = useState(false);
+ const [isOpen, setIsOpen] = useState([false]);
+
+ const handleToggleMobileNav = () => setExpanded((prev) => !prev);
+
+ const menuItems = [
+
+ Simple link one
+ ,
+
+ Simple link two
+ ,
+ ];
+
+ const primaryItems = [
+
+ onToggle(0, setIsOpen)}
+ menuId="nav-dropdown"
+ isOpen={isOpen[0]}
+ label="Nav Label"
+ isCurrent={true}
+ />
+
+ ,
+
+ Parent link
+ ,
+
+ Parent link
+ ,
+ ];
+
+ const secondaryItems = [
+
+ Simple link one
+ ,
+
+ Simple link two
+ ,
+ ];
+
return (
@@ -21,38 +93,36 @@ function App({ application }) {
-
-
-
-
RADFish Application
-
setExpanded((prvExpanded) => !prvExpanded)}
- label="Menu"
+ {/* Header - Uses USWDS Extended Header component */}
+
+
+
+

-
-
- Home
- ,
- ]}
- mobileExpanded={isExpanded}
- onToggleMobileNav={() =>
- setExpanded((prvExpanded) => !prvExpanded)
- }
- >
+
+
+
+
+ {/* Main Content Area */}
} />
+ {/* Add more routes here:
+ } />
+ */}
diff --git a/templates/react-javascript/src/index.css b/templates/react-javascript/src/index.css
index 3bfaa65a..c720aca3 100644
--- a/templates/react-javascript/src/index.css
+++ b/templates/react-javascript/src/index.css
@@ -1,6 +1,13 @@
-/* trussworks component styles */
-@import "@trussworks/react-uswds/lib/uswds.css";
-@import "@trussworks/react-uswds/lib/index.css";
+/* Main CSS Entry Point */
+/*
+ * Theme CSS imports (USWDS, theme-components, theme-overrides) are
+ * automatically injected by the RADFish Vite plugin into index.html.
+ *
+ * This file is for developer-specific styles only.
+ */
+
+/* Developer's page-level custom styles */
+@import "./styles/style.css";
/* normalize css */
body {
@@ -21,6 +28,30 @@ body {
width: 100%;
}
+.header-logo-link {
+ display: flex;
+ align-items: center;
+}
+
+.header-logo {
+ margin-top: 10px;
+ max-width: 210px;
+ width: 100%;
+}
+
+.usa-navbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+@media (max-width: 63.99em) {
+ .header-logo {
+ max-width: 110px;
+ }
+}
+
@media (min-width: 64em) {
.usa-nav__primary button[aria-expanded=false] span:after {
background: white;
diff --git a/templates/react-javascript/src/index.jsx b/templates/react-javascript/src/index.jsx
index 5ed01d1f..bd0efb23 100644
--- a/templates/react-javascript/src/index.jsx
+++ b/templates/react-javascript/src/index.jsx
@@ -1,12 +1,15 @@
import React from "react";
import ReactDOM from "react-dom/client";
-import "./styles/theme.css";
import App from "./App";
import { Application } from "@nmfs-radfish/radfish";
const root = ReactDOM.createRoot(document.getElementById("root"));
-const app = new Application();
+const app = new Application({
+ serviceWorker: {
+ url: "/service-worker.js",
+ },
+});
app.on("ready", async () => {
root.render(
diff --git a/templates/react-javascript/src/pages/Home.jsx b/templates/react-javascript/src/pages/Home.jsx
index 2a993a26..85dc0bd0 100644
--- a/templates/react-javascript/src/pages/Home.jsx
+++ b/templates/react-javascript/src/pages/Home.jsx
@@ -1,24 +1,81 @@
+/**
+ * Home.jsx - Welcome Page
+ *
+ * This is the default home page for your RADFish application.
+ * Replace this content with your own!
+ */
+
import "../index.css";
-import React from "react";
import { Button } from "@trussworks/react-uswds";
import { Link } from "react-router-dom";
function HomePage() {
return (
-
-

-
- Edit src/App.js and save to reload.
-
-
-
-
-
-
-
+
+
Welcome to RADFish
+
+
+ You're ready to start building your fisheries data collection application.
+ This template includes everything you need to get started.
+
+
+
Quick Start
+
+ -
+ Edit
vite.config.js to change app name and description
+
+ -
+ Edit
src/App.jsx to modify the header and navigation
+
+ -
+ Edit
src/pages/Home.jsx to change this page
+
+ -
+ Replace images in
themes/noaa-theme/assets/ to change logo and favicon
+
+
+
+
What's Included
+
+ -
+ USWDS Components - U.S. Web Design System via react-uswds
+
+ -
+ Offline Storage - IndexedDB for offline-first data collection
+
+ -
+ Theming - Customizable NOAA brand colors and styles
+
+ -
+ PWA Ready - Progressive Web App support for mobile deployment
+
+
+
+
Resources
+
+
+
+ {" "}
+
+
+ {" "}
+
+
+ {" "}
+
+
+
+
+
);
}
diff --git a/templates/react-javascript/src/styles/style.css b/templates/react-javascript/src/styles/style.css
new file mode 100644
index 00000000..54f8a9dd
--- /dev/null
+++ b/templates/react-javascript/src/styles/style.css
@@ -0,0 +1,37 @@
+/**
+ * Developer Page-Level Custom Styles
+ *
+ * Add your application-specific page and layout styles here.
+ * This file is for styling YOUR OWN pages/layouts, NOT theme config or component overrides.
+ *
+ * For different types of customization:
+ * - USWDS theme colors → themes/
/styles/theme.scss (Section 1)
+ * - RADFish components → themes//styles/theme.scss (Section 2)
+ * - react-uswds overrides → themes//styles/theme.scss (Section 3)
+ * - Page/layout styles → THIS FILE
+ *
+ * Examples:
+ */
+
+/* Page-specific layouts
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
+}
+*/
+
+/* Custom application components
+.fish-data-card {
+ background: var(--radfish-background);
+ border: 1px solid var(--radfish-border-light);
+ padding: 1.5rem;
+}
+
+.catch-summary-badge {
+ background-color: var(--radfish-primary);
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+}
+*/
diff --git a/templates/react-javascript/src/styles/theme.css b/templates/react-javascript/src/styles/theme.css
deleted file mode 100644
index 92b1e2d3..00000000
--- a/templates/react-javascript/src/styles/theme.css
+++ /dev/null
@@ -1,37 +0,0 @@
-:root {
- --noaa-dark-blue: #0054a4;
- --noaa-light-blue: #0093d0;
- --noaa-yellow-one: #fff3cd;
- --noaa-yellow-two: #ffeeba;
- --noaa-yellow-three: #856404;
- --noaa-accent-color: #00467f;
- --noaa-text-color: #333;
- --noaa-error-color: #af292e;
- --noaa-button-hover: #0073b6;
- --noaa-label-color: #0054a4;
- --noaa-border-dark: #565c65;
- --noaa-border-light: #ddd;
-}
-
-body {
- font-family:
- Arial Narrow,
- sans-serif;
- color: var(--noaa-text-color);
- background-color: #f4f4f4;
- line-height: 1.6;
- border-radius: 4px;
-}
-
-.header-container {
- background: var(--noaa-dark-blue);
-}
-
-.header-title {
- color: white;
-}
-
-/* Minimum size for NOAA logo must be 72px (0.75 in) */
-.header-logo {
- width: 72px;
-}
diff --git a/templates/react-javascript/themes/noaa-theme/assets/favicon.ico b/templates/react-javascript/themes/noaa-theme/assets/favicon.ico
new file mode 100644
index 00000000..6220d9e7
Binary files /dev/null and b/templates/react-javascript/themes/noaa-theme/assets/favicon.ico differ
diff --git a/templates/react-javascript/themes/noaa-theme/assets/icon-512.png b/templates/react-javascript/themes/noaa-theme/assets/icon-512.png
new file mode 100644
index 00000000..3bb3ed58
Binary files /dev/null and b/templates/react-javascript/themes/noaa-theme/assets/icon-512.png differ
diff --git a/templates/react-javascript/themes/noaa-theme/assets/logo.png b/templates/react-javascript/themes/noaa-theme/assets/logo.png
new file mode 100644
index 00000000..871ba56e
Binary files /dev/null and b/templates/react-javascript/themes/noaa-theme/assets/logo.png differ
diff --git a/templates/react-javascript/themes/noaa-theme/styles/theme.scss b/templates/react-javascript/themes/noaa-theme/styles/theme.scss
new file mode 100644
index 00000000..edfd91a4
--- /dev/null
+++ b/templates/react-javascript/themes/noaa-theme/styles/theme.scss
@@ -0,0 +1,242 @@
+/**
+ * NOAA Fisheries Brand Theme
+ *
+ * This single file contains all theme configuration:
+ * 1. USWDS Token Variables ($variables) - extracted by the RADFish plugin for USWDS configuration
+ * 2. CSS Custom Properties (:root) - additional NOAA-specific variables
+ * 3. Component Overrides - custom styles for USWDS/RADFish components
+ *
+ * NOAA Palette → USWDS Token Mapping:
+ * NOAA Dark Blue (Reflex Blue #0055A4) → primary-*
+ * NOAA Light Blue (Process Blue #0093D0) → secondary-*
+ * URCHIN (Purples) → accent-cool-*
+ * CRUSTACEAN (Orange) → accent-warm-*
+ * SEAGRASS (Greens) → success-*
+ * CORAL (Reds) → error-*
+ * Neutrals → base-*
+ */
+
+// =============================================================================
+// SECTION 1: USWDS TOKEN VARIABLES
+// These $variables are extracted by the RADFish plugin and fed to USWDS.
+// They configure the design system colors but don't produce CSS output directly.
+// =============================================================================
+
+// -----------------------------------------------------------------------------
+// BASE COLORS - NOAA Neutrals
+// -----------------------------------------------------------------------------
+$base-lightest: #ffffff;
+$base-lighter: #e8e8e8;
+$base-light: #d0d0d0;
+$base: #71767a; // USWDS gray-50 (better contrast)
+$base-dark: #7b7b7b;
+$base-darker: #4a4a4a; // Darkened for AA contrast with base-lighter links
+$base-darkest: #333333;
+
+// -----------------------------------------------------------------------------
+// PRIMARY - NOAA Dark Blue (Reflex Blue)
+// Main brand color from NOAA Fisheries Primary Palette
+// -----------------------------------------------------------------------------
+$primary-lighter: #b2def1; // 15% tint
+$primary-light: #59b9e0; // 30% tint
+$primary: #0055a4; // Reflex Blue - NOAA Dark Blue (main brand)
+$primary-vivid: #0055a4; // Reflex Blue
+$primary-dark: #00467f; // Navy Blue (Pantone 541)
+$primary-darker: #002d4d; // Darker Navy
+
+// -----------------------------------------------------------------------------
+// SECONDARY - NOAA Light Blue (Process Blue)
+// From NOAA Fisheries Primary Palette
+// -----------------------------------------------------------------------------
+$secondary-lighter: #d9eff8; // 15% tint
+$secondary-light: #59b9e0; // 30% tint
+$secondary: #007eb5; // Darkened Process Blue for AA contrast
+$secondary-vivid: #0093d0; // Process Blue (original)
+$secondary-dark: #006a99; // Darker for AA contrast with white text
+$secondary-darker: #00557a; // Darkest Process Blue
+
+// -----------------------------------------------------------------------------
+// ACCENT-COOL - URCHIN (Purples)
+// -----------------------------------------------------------------------------
+$accent-cool-lighter: #c9c9ff; // Light tint
+$accent-cool-light: #9f9fff; // Mid tint
+$accent-cool: #5a5ae6; // Darkened for AA contrast with white text
+$accent-cool-dark: #625bc4; // PMS 2725
+$accent-cool-darker: #575195; // PMS 7670
+
+// -----------------------------------------------------------------------------
+// ACCENT-WARM - CRUSTACEAN (Oranges)
+// -----------------------------------------------------------------------------
+$accent-warm-lighter: #ffd9b3; // Light tint
+$accent-warm-light: #ffb366; // Mid tint
+$accent-warm: #ff8300; // PMS 151
+$accent-warm-dark: #b54d00; // Darkened for AA contrast with white text
+$accent-warm-darker: #bc4700; // PMS 1525
+
+// -----------------------------------------------------------------------------
+// STATE COLORS
+// -----------------------------------------------------------------------------
+
+// INFO - WAVES (Teal)
+$info-lighter: #e0f7f8;
+$info-light: #5de0e6;
+$info: #1ecad3; // PMS 319
+$info-dark: #008998; // PMS 321
+$info-darker: #007078; // PMS 322
+
+// ERROR - CORAL (Red)
+$error-lighter: #ffe5e4;
+$error-light: #ff7a73;
+$error: #d02c2f; // PMS 711
+$error-dark: #b2292e; // PMS 1805
+$error-darker: #8b1f24;
+
+// WARNING - CRUSTACEAN (Orange)
+$warning-lighter: #fff3e0;
+$warning-light: #ffd9b3;
+$warning: #ff8300; // PMS 151
+$warning-dark: #d65f00; // PMS 717
+$warning-darker: #bc4700; // PMS 1525
+
+// SUCCESS - SEAGRASS (Greens)
+$success-lighter: #e8f5d6; // Light tint
+$success-light: #b8e67d; // Mid tint
+$success: #93d500; // PMS 375
+$success-dark: #4c9c2e; // PMS 362
+$success-darker: #007934; // PMS 356
+
+// -----------------------------------------------------------------------------
+// DISABLED STATE
+// -----------------------------------------------------------------------------
+$disabled-light: #e8e8e8;
+$disabled: #d0d0d0;
+$disabled-dark: #9a9a9a;
+
+
+// =============================================================================
+// SECTION 2: CSS CUSTOM PROPERTIES
+// These extend the USWDS theme tokens for NOAA-specific needs.
+// Available throughout your application via var(--noaa-*).
+// =============================================================================
+
+:root {
+ // ---------------------------------------------------------------------------
+ // NOAA PRIMARY BRAND COLORS (Direct Access)
+ // For cases where you need exact brand colors
+ // ---------------------------------------------------------------------------
+ --noaa-process-blue: #0093D0; // NOAA Light Blue
+ --noaa-reflex-blue: #0055A4; // NOAA Dark Blue
+ --noaa-navy-blue: #00467F; // PMS 541
+
+ // ---------------------------------------------------------------------------
+ // REGIONAL WEB COLORS
+ // For region-specific styling, badges, and indicators
+ // ---------------------------------------------------------------------------
+ --noaa-region-national: #0055A4; // Blue (Reflex Blue)
+ --noaa-region-west-coast: #4C9C2E; // Green (PMS 362)
+ --noaa-region-southeast: #B2292E; // Red (PMS 1805)
+ --noaa-region-alaska: #FF8300; // Orange (PMS 151)
+ --noaa-region-pacific-islands: #625BC4; // Purple (PMS 2725)
+ --noaa-region-mid-atlantic: #625BC4; // Purple (PMS 2725)
+
+ // ---------------------------------------------------------------------------
+ // THEME PALETTE QUICK ACCESS
+ // All six NOAA theme palettes for easy reference
+ // ---------------------------------------------------------------------------
+
+ // OCEANS (mapped to primary-*)
+ --noaa-oceans-light: #0093D0;
+ --noaa-oceans: #0055A4;
+ --noaa-oceans-dark: #00467F;
+
+ // WAVES (mapped to accent-cool-*)
+ --noaa-waves-light: #1ECAD3;
+ --noaa-waves: #008998;
+ --noaa-waves-dark: #007078;
+
+ // SEAGRASS (mapped to success-*)
+ --noaa-seagrass-light: #93D500;
+ --noaa-seagrass: #4C9C2E;
+ --noaa-seagrass-dark: #007934;
+
+ // CRUSTACEAN (mapped to accent-warm-*)
+ --noaa-crustacean-light: #FF8300;
+ --noaa-crustacean: #D65F00;
+ --noaa-crustacean-dark: #BC4700;
+
+ // CORAL (mapped to secondary-*)
+ --noaa-coral-light: #FF4438;
+ --noaa-coral: #D02C2F;
+ --noaa-coral-dark: #B2292E;
+}
+
+
+// =============================================================================
+// SECTION 3: COMPONENT OVERRIDES
+// Customize RADFish and USWDS components here.
+//
+// Available CSS variables (injected by RADFish plugin):
+// var(--radfish-color-primary) - Primary brand color
+// var(--radfish-color-primary-dark) - Darker primary
+// var(--radfish-color-secondary) - Secondary color
+// var(--radfish-color-secondary-dark) - Darker secondary
+// var(--radfish-color-accent-cool) - Cool accent (teal)
+// var(--radfish-color-accent-warm) - Warm accent (orange)
+// var(--radfish-color-base-lightest) - Lightest gray
+// var(--radfish-color-error) - Error red
+// var(--radfish-color-success) - Success green
+// var(--radfish-color-warning) - Warning orange
+//
+// Available react-uswds components to override:
+// Layout & Navigation: usa-header, usa-footer, usa-sidenav, usa-breadcrumb
+// Forms & Inputs: usa-button, usa-input, usa-checkbox, usa-radio, usa-select
+// Content & Display: usa-card, usa-alert, usa-table, usa-list, usa-accordion
+// Interactive: usa-modal, usa-tooltip, usa-pagination, usa-language-selector
+//
+// See: https://designsystem.digital.gov/components/overview/
+// =============================================================================
+
+// -----------------------------------------------------------------------------
+// HEADER
+// -----------------------------------------------------------------------------
+
+/* Header Background */
+header.usa-header {
+ // background-color: var(--radfish-color-primary);
+}
+
+/* Header Logo Width - Minimum 72px for agency logos */
+
+// -----------------------------------------------------------------------------
+// EXAMPLE OVERRIDES (uncomment to use)
+// -----------------------------------------------------------------------------
+
+/* Example: Regional badge styling */
+/*
+.region-badge {
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: white;
+}
+
+.region-badge--national { background-color: var(--noaa-region-national); }
+.region-badge--west-coast { background-color: var(--noaa-region-west-coast); }
+.region-badge--southeast { background-color: var(--noaa-region-southeast); }
+.region-badge--alaska { background-color: var(--noaa-region-alaska); }
+.region-badge--pacific-islands { background-color: var(--noaa-region-pacific-islands); }
+.region-badge--mid-atlantic { background-color: var(--noaa-region-mid-atlantic); }
+*/
+
+/* Example: Custom button styles */
+/* .usa-button {
+ border-radius: 8px;
+ font-weight: 600;
+} */
+
+/* Example: Custom card styles */
+/* .usa-card {
+ border-color: color("base-light");
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+} */
diff --git a/templates/react-javascript/vite.config.js b/templates/react-javascript/vite.config.js
index e5d836a4..1c09487a 100644
--- a/templates/react-javascript/vite.config.js
+++ b/templates/react-javascript/vite.config.js
@@ -1,11 +1,58 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
+import { radFishThemePlugin } from "./plugins/vite-plugin-radfish-theme.js";
-export default defineConfig((env) => ({
+/**
+ * RADFish Theme Configuration
+ *
+ * IMPORTANT: Change theme settings in ONE place:
+ * themes/noaa-theme/styles/theme.scss
+ *
+ * The radFishThemePlugin automatically:
+ * - Parses theme.scss and generates _uswds-generated.scss
+ * - Injects CSS variables into index.html
+ * - Updates index.html meta tags
+ * - Generates manifest.json (dev + build)
+ *
+ * Usage:
+ * radFishThemePlugin("noaa-theme") // Use default NOAA theme
+ * radFishThemePlugin("noaa-theme", { app: {...} }) // With app config overrides
+ *
+ * Available themes (in themes/ folder):
+ * - noaa-theme: Default NOAA branding with USWDS colors
+ */
+
+// Define config overrides here (app name, description)
+// NOTE: Colors automatically come from themes//styles/theme.scss (Section 1)
+const configOverrides = {
+ app: {
+ name: "RADFISH Boilerplate",
+ shortName: "RADFISH Boilerplate",
+ description: "Offline-first fisheries data collection built with RADFish",
+ },
+};
+
+export default defineConfig({
base: "/",
+ css: {
+ preprocessorOptions: {
+ scss: {
+ includePaths: ["node_modules/@uswds/uswds/packages"],
+ // Suppress USWDS deprecation warnings about global built-in functions
+ // These are from USWDS using map-merge() which will be fixed in Dart Sass 3.0.0
+ quietDeps: true,
+ },
+ },
+ },
plugins: [
+ // RADFish theme plugin - provides:
+ // - import.meta.env.RADFISH_* constants
+ // - CSS variable injection
+ // - manifest.json generation on build
+ radFishThemePlugin("noaa-theme", configOverrides),
react(),
+ // VitePWA for service worker
VitePWA({
devOptions: {
enabled: process.env.NODE_ENV === "development",
@@ -16,68 +63,8 @@ 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.json is generated by radFishThemePlugin (dev + build)
+ manifest: false,
}),
],
server: {
@@ -89,4 +76,4 @@ export default defineConfig((env) => ({
setupFiles: "./src/__tests__/setup.js",
environment: "jsdom",
},
-}));
+});