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>/, + `<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 */} +
+
+ + {import.meta.env.RADFISH_APP_NAME} -
- - 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 ( -
- RADFish logo -

- 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", }, -})); +});