Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad8453d9be | ||
|
|
f739684d61 | ||
|
|
9035122f19 | ||
|
|
cc60a982ea | ||
|
|
349cf7676c | ||
|
|
1e80deb967 | ||
|
|
6b9ba669fc | ||
|
|
50b1024d60 | ||
|
|
6dc46efb90 | ||
|
|
b8fbf674bb | ||
|
|
6b7845baf2 | ||
|
|
743ed22eaf | ||
|
|
25b13f900d | ||
|
|
bed9c0fc5d |
No files matched your search
@@ -49,9 +49,10 @@ MUI components + generated screens
|
||||
|
||||
### Key Entry Points
|
||||
|
||||
- `src/App.tsx` - Main app, authentication, route generation from metadata, theme provider
|
||||
- `src/App.tsx` - Main app, authentication, route generation from metadata, theme provider, BrandedHeaderBar
|
||||
- `src/QContext.tsx` - Global state (accentColor, tableMetaData, modalStack, branding)
|
||||
- `src/qqq/utils/qqq/Client.ts` - Singleton wrapper for `@qrunio/qqq-frontend-core` API client
|
||||
- `src/qqq/utils/themeUtils.ts` - Theme CSS variable injection and font loading
|
||||
|
||||
### Source Structure
|
||||
|
||||
@@ -61,7 +62,7 @@ src/qqq/
|
||||
│ ├── base/ # Design tokens: colors.ts, typography.ts, boxShadows.ts
|
||||
│ └── components/ # 55 MUI component style overrides
|
||||
├── components/
|
||||
│ ├── horseshoe/ # Core layout: SideNav, NavBar, Footer, Breadcrumbs
|
||||
│ ├── horseshoe/ # Core layout: SideNav, NavBar, Footer, Breadcrumbs, BrandedHeaderBar
|
||||
│ ├── forms/ # DynamicForm, EntityForm, field components
|
||||
│ ├── widgets/ # Dashboard widgets, charts, statistics
|
||||
│ ├── query/ # Table filtering components
|
||||
@@ -84,35 +85,131 @@ Routes are dynamically created from `QInstance.appTree` metadata in `App.tsx`:
|
||||
- Tables get CRUD routes: `/app/{table}`, `/app/{table}/create`, `/app/{table}/:id`, `/app/{table}/:id/edit`
|
||||
- Processes get execution routes under their parent table or app
|
||||
|
||||
## Theming Architecture
|
||||
## Pluggable Themes System (Implemented)
|
||||
|
||||
### Current State
|
||||
### Overview
|
||||
|
||||
Theme configuration is centralized in `src/qqq/assets/theme/` with MUI's `createTheme()`:
|
||||
The dashboard supports **runtime theme customization** via CSS custom properties. Theme configuration is defined in `QThemeMetaData` on the Java backend and injected as `--qqq-*` CSS variables by `themeUtils.ts`.
|
||||
|
||||
- `base/colors.ts` - Full color palette
|
||||
- `base/typography.ts` - Font system (SF Pro Display/Roboto)
|
||||
- `components/*.ts` - Per-component MUI overrides
|
||||
### Theme Configuration (Java Backend)
|
||||
|
||||
The theme is created in `src/qqq/components/legacy/Theme.ts` and applied via `<ThemeProvider>` in App.tsx.
|
||||
```java
|
||||
qInstance.setTheme(new QThemeMetaData()
|
||||
.withPrimaryColor("#1976D2")
|
||||
.withSidebarBackgroundColor("#1E1E2D")
|
||||
.withBrandedHeaderEnabled(true)
|
||||
.withBrandedHeaderTagline("My Application"));
|
||||
```
|
||||
|
||||
### Runtime Customization
|
||||
### Available Theme Properties (60+)
|
||||
|
||||
Limited runtime theming via `QBrandingMetaData` from backend:
|
||||
- `accentColor` - Applied to `QContext.accentColor`
|
||||
- `logo`, `icon` - Sidebar and favicon
|
||||
- `appName` - Display name
|
||||
| Category | Properties |
|
||||
|----------|------------|
|
||||
| **Core Colors** | `primaryColor`, `secondaryColor`, `backgroundColor`, `surfaceColor`, `textPrimary`, `textSecondary`, `errorColor`, `warningColor`, `successColor`, `infoColor` |
|
||||
| **Typography Base** | `fontFamily`, `headerFontFamily`, `monoFontFamily`, `fontSizeBase`, `fontWeightLight`, `fontWeightRegular`, `fontWeightMedium`, `fontWeightBold` |
|
||||
| **Typography Variants** | `typographyH1FontSize`, `typographyH1FontWeight`, `typographyH1LineHeight`, `typographyH1LetterSpacing`, `typographyH1TextTransform` (same pattern for H2-H6, Body1, Body2, Button, Caption) |
|
||||
| **Branded Header** | `brandedHeaderEnabled`, `brandedHeaderBackgroundColor`, `brandedHeaderTextColor`, `brandedHeaderLogoPath`, `brandedHeaderLogoAltText`, `brandedHeaderHeight`, `brandedHeaderTagline` |
|
||||
| **App Bar** | `appBarBackgroundColor`, `appBarTextColor` |
|
||||
| **Sidebar** | `sidebarBackgroundColor`, `sidebarTextColor`, `sidebarIconColor`, `sidebarSelectedBackgroundColor`, `sidebarSelectedTextColor`, `sidebarHoverBackgroundColor`, `sidebarDividerColor` |
|
||||
| **Tables** | `tableHeaderBackgroundColor`, `tableHeaderTextColor`, `tableRowHoverColor`, `tableRowSelectedColor`, `tableBorderColor` |
|
||||
| **General** | `dividerColor`, `borderColor`, `cardBorderColor`, `borderRadius`, `density`, `iconStyle` |
|
||||
| **Assets** | `logoPath`, `iconPath`, `faviconPath` |
|
||||
| **Custom** | `customCss` (raw CSS injection) |
|
||||
|
||||
### Known Issues for Theming Work
|
||||
### CSS Variables
|
||||
|
||||
1. **Hardcoded colors scattered in components** - Search for `#[0-9a-fA-F]{3,6}`, `rgb(`, `rgba(` in TSX files
|
||||
2. **~900 lines of CSS overrides** in `src/qqq/styles/qqq-override-styles.css`
|
||||
3. **Unused dark theme** - `src/qqq/assets/theme-dark/` exists but is never loaded
|
||||
4. **No CSS custom properties** - Theme is compile-time only
|
||||
All properties become CSS variables with `--qqq-` prefix:
|
||||
- `primaryColor` → `--qqq-primary-color`
|
||||
- `sidebarBackgroundColor` → `--qqq-sidebar-background-color`
|
||||
|
||||
### Pluggable Themes Design
|
||||
Additional generated variables:
|
||||
- Grey scale: `--qqq-grey-100` through `--qqq-grey-900`
|
||||
- Charts: `--qqq-chart-grid-color`, `--qqq-chart-text-color`, `--qqq-chart-background-color`
|
||||
- Links: `--qqq-link-color`
|
||||
- Spacing: `--qqq-spacing-base`, `--qqq-spacing-small`, `--qqq-spacing-medium`, `--qqq-spacing-large`
|
||||
- Actions: `--qqq-action-active`, `--qqq-action-hover`, `--qqq-action-selected`, `--qqq-action-disabled`
|
||||
- Status variants: `--qqq-info-light`, `--qqq-info-dark`, etc.
|
||||
|
||||
See `docs/PLUGGABLE_THEMES_DESIGN.md` for the comprehensive plan to implement runtime-swappable themes via CSS custom properties and a new `@qrunio/qqq-theme-default` package.
|
||||
Component-specific variables (override via customCss):
|
||||
- Stepper: `--qqq-stepper-inactive-color`
|
||||
- Tooltip: `--qqq-tooltip-background-color`, `--qqq-tooltip-text-color`, `--qqq-tooltip-shadow`
|
||||
- Inputs: `--qqq-input-border-color`
|
||||
- Menu: `--qqq-menu-hover-color`
|
||||
- Switch: `--qqq-switch-track-color`
|
||||
|
||||
### Key Files
|
||||
|
||||
- `src/qqq/utils/themeUtils.ts` - CSS variable injection, font loading
|
||||
- `src/qqq/components/horseshoe/BrandedHeaderBar.tsx` - Optional header banner
|
||||
- `src/qqq/styles/qqq-override-styles.css` - CSS variable overrides for MUI components
|
||||
|
||||
### Documentation
|
||||
|
||||
- `docs/QQQ_THEMING_GUIDE.md` - Complete theming reference for LLMs/developers
|
||||
- `docs/QQQ_CSS_SELECTORS_GUIDE.md` - CSS selector reference for targeting UI elements
|
||||
|
||||
## CSS Selectors System (Implemented)
|
||||
|
||||
### Overview
|
||||
|
||||
All major UI elements have stable `data-qqq-id` attributes for targeted styling via `QThemeMetaData.customCss`. IDs are generated from element names using `sanitizeId()` which converts to lowercase and replaces non-alphanumeric characters with hyphens.
|
||||
|
||||
### Key Files
|
||||
|
||||
- `src/qqq/utils/qqqIdUtils.ts` - ID sanitization utilities
|
||||
- `src/qqq/context/QqqIdContext.tsx` - Optional scope context provider
|
||||
|
||||
### Selector Patterns
|
||||
|
||||
| Element | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Buttons | `button-{text}` | `button-save` |
|
||||
| Icon buttons | `button-icon-{icon}` | `button-icon-delete` |
|
||||
| Menu items | `menu-item-{text}` | `menu-item-bulk-edit` |
|
||||
| Nav items | `sidenav-{name}` | `sidenav-orders` |
|
||||
| Inputs | `input-{field}` | `input-first-name` |
|
||||
| Selects | `select-{field}` | `select-status` |
|
||||
| Table headers | `table-header-{field}` | `table-header-order-id` |
|
||||
| Record view | `record-view-{element}-{table}` | `record-view-header-orders` |
|
||||
| Record create | `record-create-{element}-{table}` | `record-create-title-person` |
|
||||
| Record edit | `record-edit-{element}-{table}` | `record-edit-avatar-customer` |
|
||||
| Sections | `record-section-{name}` | `record-section-identity` |
|
||||
| Form sections | `form-section-{name}` | `form-section-details` |
|
||||
| Sidebar items | `sidebar-item-{name}` | `sidebar-item-identity` |
|
||||
| Table cards | `table-card-{name}` | `table-card-orders` |
|
||||
| Process cards | `process-card-{name}` | `process-card-bulk-load` |
|
||||
| App headers | `app-header-{name}` | `app-header-inventory` |
|
||||
|
||||
### Example CSS Usage
|
||||
|
||||
```css
|
||||
/* Hide delete menu item */
|
||||
[data-qqq-id="menu-item-delete"] { display: none; }
|
||||
|
||||
/* Style specific table view header */
|
||||
[data-qqq-id="record-view-header-orders"] {
|
||||
background: linear-gradient(to right, #1976D2, #42A5F5);
|
||||
}
|
||||
|
||||
/* Style all sidebar items */
|
||||
[data-qqq-id^="sidebar-item-"] {
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
```
|
||||
|
||||
## Cross-Repository Dependencies
|
||||
|
||||
This project is part of a three-repo theming system:
|
||||
|
||||
| Repository | Artifact | Current Version |
|
||||
|------------|----------|-----------------|
|
||||
| qqq (backend-core) | `com.kingsrook.qqq:qqq-backend-core` | `0.36.0-SNAPSHOT` |
|
||||
| qqq-frontend-core | `@qrunio/qqq-frontend-core` | `1.4.2-SNAPSHOT` |
|
||||
| qqq-frontend-material-dashboard | `com.kingsrook.qqq:qqq-frontend-material-dashboard` | `0.36.0-pluggable-themes-css-selectors-SNAPSHOT` |
|
||||
|
||||
**Feature branch version:** `0.36.0-pluggable-themes-css-selectors-SNAPSHOT` (for testing pluggable themes + CSS selectors)
|
||||
|
||||
**Dependency order for commits/publishes:** backend-core → frontend-core → dashboard
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -188,10 +285,10 @@ QQQ backend supports a `material-dashboard-overlay/` directory for serving custo
|
||||
|
||||
3. **Frontend resolution**: Assets are served at web root, and `resolveAssetUrl()` in `PathUtils.ts` handles base path resolution.
|
||||
|
||||
### What the Overlay Does vs. What Theming Would Do
|
||||
### What the Overlay Does vs. What Pluggable Themes Does
|
||||
|
||||
| Capability | Overlay (Current) | Pluggable Themes (Proposed) |
|
||||
|------------|-------------------|----------------------------|
|
||||
| Capability | Overlay | Pluggable Themes |
|
||||
|------------|---------|------------------|
|
||||
| Logo/icon | Yes | Yes |
|
||||
| Single accent color | Yes | Yes |
|
||||
| Full color palette | No | Yes |
|
||||
@@ -201,4 +298,71 @@ QQQ backend supports a `material-dashboard-overlay/` directory for serving custo
|
||||
| Dark mode | No | Yes |
|
||||
| Runtime switching | Partial (accentColor) | Full |
|
||||
|
||||
The overlay system handles **branding assets** (logos, icons). The pluggable themes design in `docs/PLUGGABLE_THEMES_DESIGN.md` addresses **comprehensive styling** (colors, typography, shadows, component overrides) that goes beyond what the overlay can provide.
|
||||
The overlay system handles **branding assets** (logos, icons). Pluggable themes handles **comprehensive styling** (colors, typography, shadows, component overrides).
|
||||
|
||||
## Selenium Testing
|
||||
|
||||
### Test Architecture
|
||||
|
||||
Tests use a two-server setup:
|
||||
- **React dev server** on port 3001 - serves the dashboard frontend
|
||||
- **Javalin mock server** on port 8001 - serves fixture JSON responses
|
||||
|
||||
```bash
|
||||
# Start React dev server for tests
|
||||
PORT=3001 REACT_APP_PROXY_LOCALHOST_PORT=8001 npm start
|
||||
|
||||
# Run theme tests (in another terminal)
|
||||
QQQ_SELENIUM_HEADLESS=true mvn test -Dtest=ThemeIT
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
|
||||
CircleCI uses `qqq-orb` which automatically:
|
||||
1. Starts React dev server on port 3001
|
||||
2. Waits for server readiness
|
||||
3. Runs Maven tests with headless Chrome
|
||||
|
||||
### Key Test Classes
|
||||
|
||||
- `QBaseSeleniumTest` - Base class, starts Javalin on 8001
|
||||
- `QSeleniumJavalin` - Serves fixture JSON from `src/test/resources/fixtures/`
|
||||
- `ServerHealthChecker` - Waits for React server readiness
|
||||
- `ThemeIT` - Tests theme CSS variable injection
|
||||
|
||||
### Fixtures
|
||||
|
||||
Test fixtures in `src/test/resources/fixtures/metaData/`:
|
||||
- `index.json` - Default metaData response
|
||||
- `withFullCustomTheme.json` - Theme with all properties set
|
||||
- `withBrandedHeader.json` - Theme with branded header enabled
|
||||
|
||||
## Session State & Continuity
|
||||
|
||||
**IMPORTANT:** Never use `~/.claude/session-state.md`. Always use local repo files.
|
||||
|
||||
When user says "continue from last session", read these files in order:
|
||||
1. `docs/SESSION_STATE.md` - Current status, recent commits, next steps
|
||||
2. `docs/TODO.md` - Active and completed tasks
|
||||
3. `docs/TODO-hardcoded-colors.md` - Detailed color conversion tasks (if working on theming)
|
||||
4. This file (`CLAUDE.md`) - Project context
|
||||
|
||||
### Key Documentation Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/SESSION_STATE.md` | Current work status, branch, version, recent commits |
|
||||
| `docs/TODO.md` | Task tracking for current feature |
|
||||
| `docs/TODO-hardcoded-colors.md` | Detailed hardcoded color conversion checklist |
|
||||
| `docs/QQQ_THEMING_GUIDE.md` | Complete theming reference |
|
||||
| `docs/QQQ_CSS_SELECTORS_GUIDE.md` | CSS selector patterns reference |
|
||||
| `docs/PLAN-css-selectors.md` | CSS selectors implementation plan (complete) |
|
||||
|
||||
### Publishing Snapshots
|
||||
|
||||
Use the gitops publish script to create a tagged snapshot:
|
||||
```bash
|
||||
/Users/james.maes/.local/bin/gitops-publish.sh
|
||||
```
|
||||
|
||||
This creates a `publish-{commit}` tag that triggers CI/CD to build and publish the JAR.
|
||||
@@ -0,0 +1,65 @@
|
||||
# PLAN: Stable CSS Selectors via `data-qqq-*` Attributes
|
||||
|
||||
**Status:** COMPLETE
|
||||
|
||||
## Goal
|
||||
|
||||
Add stable, semantic CSS selectors so QQQ app developers can target specific UI elements in `QThemeMetaData.customCss`.
|
||||
|
||||
## Approach
|
||||
|
||||
- **Auto-generate** unique IDs for key interactive elements (buttons, inputs, nav items, menu items, table headers)
|
||||
- **Require explicit `qqqId`** prop for custom overrides
|
||||
- ID format: `data-qqq-id="{type}-{identifier}"`
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
### New Files Created
|
||||
- `src/qqq/utils/qqqIdUtils.ts` - ID sanitization utilities
|
||||
- `src/qqq/context/QqqIdContext.tsx` - Optional scope context provider
|
||||
- `docs/QQQ_CSS_SELECTORS_GUIDE.md` - Complete selector reference
|
||||
|
||||
### Components Modified
|
||||
|
||||
| Component | Selectors Added |
|
||||
|-----------|-----------------|
|
||||
| MDButton | `button-{text}`, `button-icon-{icon}` |
|
||||
| SideNav | `sidenav-root`, `sidenav-{name}`, `sidenav-logout-button` |
|
||||
| DynamicFormField | `input-{field}` |
|
||||
| DynamicSelect | `select-{field}` |
|
||||
| DataGridUtils | `table-header-{field}` |
|
||||
| QueryScreenActionMenu | `menu-item-{text}` |
|
||||
| RecordView | `record-view-*`, `record-section-*`, `menu-item-*` |
|
||||
| EntityForm | `record-{mode}-*`, `form-section-*` |
|
||||
| RecordSidebar | `record-sidebar`, `sidebar-item-*` |
|
||||
| Home | `app-header-*`, `app-section-*`, `table-card-*`, `process-card-*` |
|
||||
|
||||
## Selector Patterns
|
||||
|
||||
| Element | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Buttons | `button-{text}` | `button-save` |
|
||||
| Icon buttons | `button-icon-{icon}` | `button-icon-delete` |
|
||||
| Menu items | `menu-item-{text}` | `menu-item-bulk-edit` |
|
||||
| Nav items | `sidenav-{name}` | `sidenav-orders` |
|
||||
| Inputs | `input-{field}` | `input-first-name` |
|
||||
| Selects | `select-{field}` | `select-status` |
|
||||
| Table headers | `table-header-{field}` | `table-header-order-id` |
|
||||
| Record view | `record-view-{element}-{table}` | `record-view-header-orders` |
|
||||
| Sections | `record-section-{name}` | `record-section-identity` |
|
||||
|
||||
## Example CSS Usage
|
||||
|
||||
```css
|
||||
[data-qqq-id="menu-item-delete"] { display: none; }
|
||||
[data-qqq-id="record-view-header-orders"] { background: #1976D2; }
|
||||
[data-qqq-id^="sidebar-item-"] { border-left: 3px solid transparent; }
|
||||
```
|
||||
|
||||
## Explicit Override
|
||||
|
||||
All components accept `qqqId` prop:
|
||||
```tsx
|
||||
<MDButton qqqId="custom-action">Do Something</MDButton>
|
||||
// Result: data-qqq-id="button-custom-action"
|
||||
```
|
||||
@@ -0,0 +1,476 @@
|
||||
# PLAN: Pluggable Themes - MUI ThemeProvider Refactor
|
||||
|
||||
## Overview
|
||||
|
||||
Refactor the pluggable themes implementation from CSS-variable-override approach to MUI ThemeProvider-first approach. The goal is to eliminate the 1200-line CSS override file by letting MUI style its own components correctly from the start.
|
||||
|
||||
## Git Branch Surgery
|
||||
|
||||
### Current State
|
||||
- `develop` branch contains 42 commits of CSS variable theme work (since 0.35.0)
|
||||
- All commits have been pushed to `origin/develop`
|
||||
- Commit `759581b` is the version bump to 0.36.0-SNAPSHOT (clean base)
|
||||
- Commit `f807698` is the 0.35.0 release
|
||||
|
||||
### Target State
|
||||
- CSS variable work preserved on `feature/pluggable-themes-v1-css-vars`
|
||||
- `develop` reset to `759581b` (0.36.0-SNAPSHOT, pre-theme work)
|
||||
- New `feature/pluggable-themes-v2-mui` branch for MUI refactor
|
||||
|
||||
### Step 1: Create Branch to Preserve CSS Variable Work
|
||||
```bash
|
||||
# From develop (which has all the CSS var work)
|
||||
git checkout develop
|
||||
git checkout -b feature/pluggable-themes-v1-css-vars
|
||||
|
||||
# Push to origin to preserve it
|
||||
git push -u origin feature/pluggable-themes-v1-css-vars
|
||||
```
|
||||
|
||||
### Step 2: Reset develop to Pre-Theme State
|
||||
```bash
|
||||
# Switch back to develop
|
||||
git checkout develop
|
||||
|
||||
# Reset to the version bump commit (before any theme work)
|
||||
git reset --hard 759581b
|
||||
|
||||
# Force push to origin (CAUTION: coordinate with team first)
|
||||
git push --force-with-lease origin develop
|
||||
```
|
||||
|
||||
### Step 3: Create MUI Refactor Branch
|
||||
```bash
|
||||
# Create new feature branch from clean develop
|
||||
git checkout -b feature/pluggable-themes-v2-mui
|
||||
|
||||
# Push to origin
|
||||
git push -u origin feature/pluggable-themes-v2-mui
|
||||
```
|
||||
|
||||
### Verification
|
||||
```bash
|
||||
# Verify develop is clean
|
||||
git log --oneline develop -5
|
||||
# Should show: 759581b as HEAD
|
||||
|
||||
# Verify v1 branch has all the CSS var work
|
||||
git log --oneline feature/pluggable-themes-v1-css-vars -5
|
||||
# Should show: 385130a (chore: ignore Playwright test artifacts) as HEAD
|
||||
|
||||
# Verify v2 branch is ready for new work
|
||||
git log --oneline feature/pluggable-themes-v2-mui -5
|
||||
# Should show: 759581b as HEAD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Infrastructure Setup (Day 1)
|
||||
|
||||
#### 1.1 Copy Essential Files from v1 Branch
|
||||
Files to bring over (they contain valuable work):
|
||||
```
|
||||
src/qqq/utils/themeUtils.ts # DEFAULT_THEME, helper functions
|
||||
src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/tests/ThemeIT.java
|
||||
src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/tests/ThemeComponentRenderingIT.java
|
||||
src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/tests/ThemeMuiComponentIT.java
|
||||
src/test/resources/fixtures/metaData/withFullCustomTheme.json
|
||||
src/test/resources/fixtures/metaData/withBrandedHeader.json
|
||||
src/test/resources/fixtures/metaData/withCompactDensity.json
|
||||
docs/blog-drafts/* # Blog posts for reference
|
||||
```
|
||||
|
||||
```bash
|
||||
# From feature/pluggable-themes-v2-mui
|
||||
git checkout feature/pluggable-themes-v1-css-vars -- src/qqq/utils/themeUtils.ts
|
||||
git checkout feature/pluggable-themes-v1-css-vars -- "src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/tests/Theme*.java"
|
||||
git checkout feature/pluggable-themes-v1-css-vars -- "src/test/resources/fixtures/metaData/with*.json"
|
||||
git checkout feature/pluggable-themes-v1-css-vars -- docs/blog-drafts/
|
||||
```
|
||||
|
||||
#### 1.2 Create createDynamicTheme.ts
|
||||
New file: `src/qqq/utils/createDynamicTheme.ts`
|
||||
|
||||
```typescript
|
||||
import { createTheme, Theme } from "@mui/material";
|
||||
import { QThemeMetaData } from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
import { DEFAULT_THEME } from "./themeUtils";
|
||||
|
||||
export function createDynamicTheme(themeData?: QThemeMetaData): Theme {
|
||||
// Merge with defaults
|
||||
const theme = { ...DEFAULT_THEME, ...filterUndefined(themeData) };
|
||||
|
||||
return createTheme({
|
||||
palette: {
|
||||
primary: { main: theme.primaryColor },
|
||||
secondary: { main: theme.secondaryColor },
|
||||
background: {
|
||||
default: theme.backgroundColor,
|
||||
paper: theme.surfaceColor,
|
||||
},
|
||||
text: {
|
||||
primary: theme.textPrimary,
|
||||
secondary: theme.textSecondary,
|
||||
},
|
||||
error: { main: theme.errorColor },
|
||||
warning: { main: theme.warningColor },
|
||||
success: { main: theme.successColor },
|
||||
info: { main: theme.infoColor },
|
||||
},
|
||||
typography: {
|
||||
fontFamily: theme.fontFamily,
|
||||
h1: {
|
||||
fontSize: theme.typographyH1FontSize,
|
||||
fontWeight: theme.typographyH1FontWeight,
|
||||
lineHeight: theme.typographyH1LineHeight,
|
||||
letterSpacing: theme.typographyH1LetterSpacing,
|
||||
},
|
||||
// ... h2-h6, body1, body2, button, caption
|
||||
},
|
||||
shape: {
|
||||
borderRadius: parseInt(theme.borderRadius) || 8,
|
||||
},
|
||||
components: {
|
||||
// Component overrides go here - see Phase 2
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function filterUndefined(obj?: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!obj) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([_, v]) => v !== undefined)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 Create injectIslandVariables.ts
|
||||
New file: `src/qqq/utils/injectIslandVariables.ts`
|
||||
|
||||
This injects CSS variables ONLY for sidebar, branded header, and non-MUI components.
|
||||
|
||||
```typescript
|
||||
import { QThemeMetaData } from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
import { DEFAULT_THEME } from "./themeUtils";
|
||||
|
||||
const CSS_VAR_PREFIX = "--qqq-";
|
||||
|
||||
export function injectIslandVariables(themeData?: QThemeMetaData): void {
|
||||
const root = document.documentElement;
|
||||
const theme = { ...DEFAULT_THEME, ...filterUndefined(themeData) };
|
||||
|
||||
// Sidebar island
|
||||
setVar(root, "sidebar-background-color", theme.sidebarBackgroundColor);
|
||||
setVar(root, "sidebar-text-color", theme.sidebarTextColor);
|
||||
setVar(root, "sidebar-icon-color", theme.sidebarIconColor);
|
||||
setVar(root, "sidebar-selected-background-color", theme.sidebarSelectedBackgroundColor);
|
||||
setVar(root, "sidebar-selected-text-color", theme.sidebarSelectedTextColor);
|
||||
setVar(root, "sidebar-hover-background-color", theme.sidebarHoverBackgroundColor);
|
||||
setVar(root, "sidebar-divider-color", theme.sidebarDividerColor);
|
||||
|
||||
// Branded header island
|
||||
setVar(root, "branded-header-background-color", theme.brandedHeaderBackgroundColor);
|
||||
setVar(root, "branded-header-text-color", theme.brandedHeaderTextColor);
|
||||
setVar(root, "branded-header-height", theme.brandedHeaderHeight);
|
||||
|
||||
// DataGrid (has its own theming needs)
|
||||
setVar(root, "table-header-background-color", theme.tableHeaderBackgroundColor);
|
||||
setVar(root, "table-header-text-color", theme.tableHeaderTextColor);
|
||||
setVar(root, "table-row-hover-color", theme.tableRowHoverColor);
|
||||
setVar(root, "table-row-selected-color", theme.tableRowSelectedColor);
|
||||
setVar(root, "table-border-color", theme.tableBorderColor);
|
||||
|
||||
// Icon style (for material icons variant selection)
|
||||
setVar(root, "icon-style", theme.iconStyle);
|
||||
}
|
||||
|
||||
function setVar(root: HTMLElement, name: string, value: unknown): void {
|
||||
if (value != null) {
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}${name}`, String(value));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 Update App.tsx Theme Integration
|
||||
Modify `src/App.tsx` to use the new theme builder:
|
||||
|
||||
```typescript
|
||||
// Old imports
|
||||
import { injectThemeVariables } from "qqq/utils/themeUtils";
|
||||
import { createQqqTheme } from "qqq/components/legacy/Theme";
|
||||
|
||||
// New imports
|
||||
import { createDynamicTheme } from "qqq/utils/createDynamicTheme";
|
||||
import { injectIslandVariables } from "qqq/utils/injectIslandVariables";
|
||||
|
||||
// In the metadata loading effect:
|
||||
useEffect(() => {
|
||||
// ... existing metadata loading ...
|
||||
|
||||
// OLD:
|
||||
// injectThemeVariables(metaData.theme);
|
||||
// setTheme(createQqqTheme());
|
||||
|
||||
// NEW:
|
||||
const muiTheme = createDynamicTheme(metaData.theme);
|
||||
setTheme(muiTheme);
|
||||
injectIslandVariables(metaData.theme);
|
||||
|
||||
}, [metaData]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Migrate Component Overrides (Days 1-2)
|
||||
|
||||
The goal is to move component styling from CSS overrides into MUI's theme.components.
|
||||
|
||||
#### 2.1 Component Override Migration Order
|
||||
|
||||
| Priority | Component | Current Location | Complexity |
|
||||
|----------|-----------|------------------|------------|
|
||||
| 1 | MuiButton | button/contained.ts, button/outlined.ts | Low - already uses CSS vars |
|
||||
| 2 | MuiCard | card/index.ts | Low |
|
||||
| 3 | MuiTextField, MuiInput | form/input.ts, form/textField.ts | Medium |
|
||||
| 4 | MuiTypography | (CSS overrides) | Medium |
|
||||
| 5 | MuiAppBar | appBar.ts | Low |
|
||||
| 6 | MuiDivider | divider.ts | Low |
|
||||
| 7 | MuiTableHead, MuiTableCell | table/*.ts | Medium |
|
||||
| 8 | MuiStepper, MuiStep* | stepper/*.ts | Medium |
|
||||
| 9 | MuiTabs, MuiTab | tabs/*.ts | Low |
|
||||
| 10 | MuiMenu, MuiMenuItem | menu/*.ts | Low |
|
||||
| 11 | MuiTooltip | tooltip.ts | Low |
|
||||
| 12 | MuiDialog* | dialog/*.ts | Low |
|
||||
| 13 | Remaining components | Various | Low-Medium |
|
||||
|
||||
#### 2.2 Migration Pattern
|
||||
|
||||
For each component:
|
||||
|
||||
1. **Read current CSS override** in `qqq-override-styles.css`
|
||||
2. **Read current component file** in `assets/theme/components/`
|
||||
3. **Identify which styles need theme values**
|
||||
4. **Add to createDynamicTheme.ts** components section
|
||||
5. **Remove from CSS override file**
|
||||
6. **Run tests** to verify
|
||||
|
||||
Example for MuiButton:
|
||||
|
||||
```typescript
|
||||
// In createDynamicTheme.ts components section:
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
borderRadius: theme.borderRadius,
|
||||
},
|
||||
containedPrimary: {
|
||||
backgroundColor: theme.primaryColor,
|
||||
color: '#fff',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.primaryColor,
|
||||
filter: 'brightness(0.9)',
|
||||
},
|
||||
},
|
||||
containedSecondary: {
|
||||
backgroundColor: theme.secondaryColor,
|
||||
color: '#fff',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.secondaryColor,
|
||||
filter: 'brightness(0.9)',
|
||||
},
|
||||
},
|
||||
outlinedPrimary: {
|
||||
borderColor: theme.primaryColor,
|
||||
color: theme.primaryColor,
|
||||
'&:hover': {
|
||||
borderColor: theme.primaryColor,
|
||||
backgroundColor: `${theme.primaryColor}10`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
#### 2.3 Files to Modify
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `src/qqq/utils/createDynamicTheme.ts` | Add component overrides progressively |
|
||||
| `src/qqq/assets/theme/components/button/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/card/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/form/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/table/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/menu/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/stepper/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/tabs/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/components/dialog/*.ts` | May delete or simplify |
|
||||
| `src/qqq/assets/theme/base/colors.ts` | Simplify - remove CSS var getters |
|
||||
| `src/qqq/components/legacy/Theme.ts` | Simplify or remove |
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Reduce CSS Override File (Day 2-3)
|
||||
|
||||
#### 3.1 Categorize Current Overrides
|
||||
|
||||
The current `qqq-override-styles.css` has ~1213 lines. Categorize:
|
||||
|
||||
| Category | Lines (est.) | Action |
|
||||
|----------|--------------|--------|
|
||||
| DataGrid-specific | ~300 | Keep (DataGrid has own theming) |
|
||||
| Sidebar (island) | ~80 | Keep (CSS vars for island) |
|
||||
| Branded Header (island) | ~30 | Keep (CSS vars for island) |
|
||||
| MUI component colors | ~400 | DELETE (handled by MUI theme) |
|
||||
| MUI component spacing/layout | ~200 | Review - some may stay |
|
||||
| Typography overrides | ~100 | DELETE (handled by MUI theme) |
|
||||
| Miscellaneous/utility | ~100 | Review case by case |
|
||||
|
||||
#### 3.2 Lines to Delete
|
||||
|
||||
Search and remove patterns like:
|
||||
```css
|
||||
/* DELETE - MUI theme handles this */
|
||||
.MuiButton-containedPrimary { background-color: var(--qqq-primary-color) !important; }
|
||||
.MuiTypography-body2:not(...) { color: var(--qqq-text-secondary) !important; }
|
||||
.MuiCard-root { border-color: var(--qqq-card-border-color) !important; }
|
||||
```
|
||||
|
||||
#### 3.3 Lines to Keep
|
||||
|
||||
Keep patterns like:
|
||||
```css
|
||||
/* KEEP - DataGrid specific */
|
||||
.MuiDataGrid-cell { font-size: 0.85rem; }
|
||||
.MuiDataGrid-columnHeaderTitle { font-size: 0.75rem; }
|
||||
|
||||
/* KEEP - Sidebar island */
|
||||
.MuiDrawer-root .MuiListItemText-primary { color: var(--qqq-sidebar-text-color); }
|
||||
|
||||
/* KEEP - Layout/structure (not theming) */
|
||||
.recordQuery .MuiDataGrid-virtualScrollerContent { min-height: calc(100vh - 450px); }
|
||||
```
|
||||
|
||||
#### 3.4 Target Metrics
|
||||
|
||||
| Metric | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Total lines | 1213 | ~300-400 |
|
||||
| `!important` count | 82 | ~10-15 |
|
||||
| `:not()` chains | 19 | ~2-3 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Testing and Validation (Day 3)
|
||||
|
||||
#### 4.1 Playwright Test Execution
|
||||
|
||||
```bash
|
||||
# Start React dev server
|
||||
PORT=3001 REACT_APP_PROXY_LOCALHOST_PORT=8001 npm start
|
||||
|
||||
# In another terminal, run theme tests
|
||||
QQQ_SELENIUM_HEADLESS=true mvn test -Dtest=ThemeIT
|
||||
QQQ_SELENIUM_HEADLESS=true mvn test -Dtest=ThemeComponentRenderingIT
|
||||
QQQ_SELENIUM_HEADLESS=true mvn test -Dtest=ThemeMuiComponentIT
|
||||
```
|
||||
|
||||
#### 4.2 Expected Test Behavior
|
||||
|
||||
The tests verify RENDERED colors, not implementation. They should pass with no changes because:
|
||||
- Tests check `getComputedStyle(element).backgroundColor`
|
||||
- Whether that color came from CSS vars or MUI theme, the rendered result is the same
|
||||
- If a test fails, it indicates a regression in the refactor
|
||||
|
||||
#### 4.3 Manual Verification Checklist
|
||||
|
||||
- [ ] Primary color applies to buttons, links, active states
|
||||
- [ ] Secondary color applies where expected
|
||||
- [ ] Background color applies to page background
|
||||
- [ ] Surface color applies to cards, paper elements
|
||||
- [ ] Text primary/secondary colors render correctly
|
||||
- [ ] Sidebar has correct background, text, icon colors
|
||||
- [ ] Sidebar hover and selected states work
|
||||
- [ ] Branded header (when enabled) has correct colors
|
||||
- [ ] DataGrid headers and rows have correct colors
|
||||
- [ ] Typography variants (h1-h6, body1, body2) look correct
|
||||
- [ ] Border radius applies to buttons, cards, inputs
|
||||
- [ ] No visual regressions in existing app screens
|
||||
|
||||
#### 4.4 CI Pipeline
|
||||
|
||||
Ensure CircleCI runs all tests:
|
||||
```bash
|
||||
# Full test suite
|
||||
mvn verify -Pci
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Inventory
|
||||
|
||||
### New Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/qqq/utils/createDynamicTheme.ts` | MUI theme builder from QThemeMetaData |
|
||||
| `src/qqq/utils/injectIslandVariables.ts` | CSS vars for sidebar/header islands only |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/App.tsx` | Use new theme builder |
|
||||
| `src/qqq/utils/themeUtils.ts` | Keep DEFAULT_THEME, remove injectThemeVariables |
|
||||
| `src/qqq/styles/qqq-override-styles.css` | Delete ~800 lines |
|
||||
| `src/qqq/assets/theme/base/colors.ts` | Simplify, remove getters |
|
||||
| `src/qqq/components/legacy/Theme.ts` | Simplify or delete |
|
||||
| `src/qqq/assets/theme/components/*.ts` | Many files simplified or deleted |
|
||||
|
||||
### Files to Delete (Maybe)
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `src/qqq/assets/theme/components/button/contained.ts` | Moved to createDynamicTheme |
|
||||
| `src/qqq/assets/theme/components/button/outlined.ts` | Moved to createDynamicTheme |
|
||||
| (evaluate each component file) | |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. All 26 Playwright tests pass
|
||||
2. CSS override file reduced from 1213 to <400 lines
|
||||
3. `!important` count reduced from 82 to <15
|
||||
4. Theme data flows directly: QThemeMetaData -> MUI Theme -> Components
|
||||
5. No visual regressions in app
|
||||
6. CI pipeline passes
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If refactor causes significant issues:
|
||||
```bash
|
||||
# The v1 CSS variable approach is preserved
|
||||
git checkout feature/pluggable-themes-v1-css-vars
|
||||
|
||||
# Or cherry-pick specific fixes back
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
| Phase | Duration | Dependencies |
|
||||
|-------|----------|--------------|
|
||||
| Git branch surgery | 30 min | None |
|
||||
| Phase 1: Infrastructure | 4-6 hours | Branch surgery complete |
|
||||
| Phase 2: Component migration | 8-12 hours | Phase 1 complete |
|
||||
| Phase 3: CSS reduction | 4-6 hours | Phase 2 complete |
|
||||
| Phase 4: Testing | 4-6 hours | Phase 3 complete |
|
||||
| Buffer for issues | 4-8 hours | |
|
||||
| **Total** | **3-5 days** | |
|
||||
@@ -0,0 +1,76 @@
|
||||
# QQQ CSS Selectors Guide
|
||||
|
||||
QQQ automatically generates stable `data-qqq-id` attributes on key UI elements, enabling precise CSS targeting via `QThemeMetaData.customCss`.
|
||||
|
||||
## ID Format
|
||||
|
||||
```
|
||||
data-qqq-id="{type}-{identifier}"
|
||||
```
|
||||
|
||||
IDs are generated from element text, field names, or labels. Values are lowercase, alphanumeric with dashes, max 50 characters.
|
||||
|
||||
## Auto-Generated Elements
|
||||
|
||||
| Element | ID Source | Example |
|
||||
|---------|-----------|---------|
|
||||
| Buttons | Text content or icon | `button-save`, `button-icon-edit` |
|
||||
| Text Inputs | Field name or label | `input-first-name` |
|
||||
| Selects | Field name or label | `select-status` |
|
||||
| Checkboxes | Field name or label | `switch-is-active` |
|
||||
| Nav Items | Name prop | `sidenav-orders` |
|
||||
| Menu Items | Text content | `menu-item-bulk-edit` |
|
||||
| Table Headers | Field name | `table-header-order-id` |
|
||||
|
||||
## CSS Usage Examples
|
||||
|
||||
```css
|
||||
/* Target specific button */
|
||||
[data-qqq-id="button-save"] {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
/* Target inputs by pattern */
|
||||
[data-qqq-id^="input-order"] {
|
||||
border-color: blue;
|
||||
}
|
||||
|
||||
/* Hide specific menu item */
|
||||
[data-qqq-id="menu-item-developer-mode"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Style table header */
|
||||
[data-qqq-id="table-header-status"] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Combine with scope context */
|
||||
[data-qqq-scope="record-view"] [data-qqq-id="button-edit"] {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
## Explicit Override
|
||||
|
||||
Components accept an optional `qqqId` prop for explicit IDs:
|
||||
|
||||
```tsx
|
||||
<MDButton qqqId="custom-action">Do Something</MDButton>
|
||||
// Result: data-qqq-id="button-custom-action"
|
||||
```
|
||||
|
||||
## Scoping Context
|
||||
|
||||
Use `QqqIdProvider` to add scope attributes for page-level targeting:
|
||||
|
||||
```tsx
|
||||
<QqqIdProvider scope="record-view">
|
||||
{/* Children get wrapped in data-qqq-scope="record-view" */}
|
||||
</QqqIdProvider>
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/qqq/utils/qqqIdUtils.ts` - ID generation utilities
|
||||
- `src/qqq/context/QqqIdContext.tsx` - Scoping context provider
|
||||
@@ -0,0 +1,301 @@
|
||||
# QQQ Theming System Guide
|
||||
|
||||
This document explains the QQQ pluggable theming system for AI assistants and developers.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
QQQ uses a **backend-driven theming model**:
|
||||
|
||||
1. **Java Backend**: Define theme in `QThemeMetaData` object
|
||||
2. **Frontend Core**: TypeScript `QThemeMetaData` interface mirrors Java class
|
||||
3. **Frontend Dashboard**: `themeUtils.ts` injects CSS custom properties at runtime
|
||||
|
||||
Theme data flows: `QThemeMetaData.java` → `/metaData` API → `QThemeMetaData.ts` → CSS variables (`--qqq-*`)
|
||||
|
||||
## Configuring a Theme (Java Backend)
|
||||
|
||||
Create a `QThemeMetaData` instance using fluent setters:
|
||||
|
||||
```java
|
||||
QThemeMetaData theme = new QThemeMetaData()
|
||||
.withPrimaryColor("#1976D2")
|
||||
.withSidebarBackgroundColor("#1E1E2D");
|
||||
|
||||
qInstance.setTheme(theme);
|
||||
```
|
||||
|
||||
All properties are optional. Omitted properties use frontend defaults.
|
||||
|
||||
---
|
||||
|
||||
## Available Theme Properties
|
||||
|
||||
### Core Color Palette
|
||||
|
||||
| Property | Purpose | Example |
|
||||
|----------|---------|---------|
|
||||
| `primaryColor` | Main brand color (buttons, links, highlights) | `#1976D2` |
|
||||
| `secondaryColor` | Secondary accent color | `#9C27B0` |
|
||||
| `backgroundColor` | Page/app background | `#F5F5F5` |
|
||||
| `surfaceColor` | Card/panel backgrounds | `#FFFFFF` |
|
||||
| `textPrimary` | Primary text color | `#212121` |
|
||||
| `textSecondary` | Muted/secondary text | `#757575` |
|
||||
| `errorColor` | Error states | `#D32F2F` |
|
||||
| `warningColor` | Warning states | `#F57C00` |
|
||||
| `successColor` | Success states | `#388E3C` |
|
||||
| `infoColor` | Info states | `#1976D2` |
|
||||
|
||||
### Typography - Base Settings
|
||||
|
||||
| Property | Purpose | Example |
|
||||
|----------|---------|---------|
|
||||
| `fontFamily` | Primary body font stack | `"Inter", "Roboto", sans-serif` |
|
||||
| `headerFontFamily` | Heading font stack | `"Poppins", "Roboto", sans-serif` |
|
||||
| `monoFontFamily` | Monospace font stack | `"JetBrains Mono", monospace` |
|
||||
| `fontSizeBase` | Base font size | `14px` |
|
||||
| `fontWeightLight` | Light weight value | `300` |
|
||||
| `fontWeightRegular` | Regular weight value | `400` |
|
||||
| `fontWeightMedium` | Medium weight value | `500` |
|
||||
| `fontWeightBold` | Bold weight value | `700` |
|
||||
|
||||
### Typography Variants
|
||||
|
||||
Each variant (h1-h6, body1, body2, button, caption) has 5 properties:
|
||||
- `FontSize` - e.g., `2.5rem`
|
||||
- `FontWeight` - e.g., `700`
|
||||
- `LineHeight` - e.g., `1.2`
|
||||
- `LetterSpacing` - e.g., `-0.02em`
|
||||
- `TextTransform` - e.g., `none`, `uppercase`
|
||||
|
||||
**Property naming pattern:** `typography{Variant}{Property}`
|
||||
|
||||
Examples:
|
||||
- `typographyH1FontSize`, `typographyH1FontWeight`, `typographyH1LineHeight`
|
||||
- `typographyBody1FontSize`, `typographyButtonTextTransform`
|
||||
|
||||
**Available variants:** `H1`, `H2`, `H3`, `H4`, `H5`, `H6`, `Body1`, `Body2`, `Button`, `Caption`
|
||||
|
||||
### Sizing & Density
|
||||
|
||||
| Property | Purpose | Values/Example |
|
||||
|----------|---------|----------------|
|
||||
| `borderRadius` | Default corner radius | `8px` |
|
||||
| `density` | UI density | `compact`, `normal`, `comfortable` |
|
||||
|
||||
### Asset Paths
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `logoPath` | Main logo (sidebar, login) |
|
||||
| `iconPath` | Small icon (favicon base) |
|
||||
| `faviconPath` | Browser favicon |
|
||||
|
||||
### Icon Style
|
||||
|
||||
| Property | Values |
|
||||
|----------|--------|
|
||||
| `iconStyle` | `filled`, `outlined`, `rounded`, `sharp`, `two-tone` |
|
||||
|
||||
### Branded Header Bar
|
||||
|
||||
Optional banner at viewport top, above the main navigation.
|
||||
|
||||
| Property | Purpose | Example |
|
||||
|----------|---------|---------|
|
||||
| `brandedHeaderEnabled` | Toggle visibility | `true` / `false` |
|
||||
| `brandedHeaderBackgroundColor` | Banner background | `#0D47A1` |
|
||||
| `brandedHeaderTextColor` | Banner text color | `#FFFFFF` |
|
||||
| `brandedHeaderLogoPath` | Logo image path | `/header-logo.png` |
|
||||
| `brandedHeaderLogoAltText` | Logo alt text | `Company Logo` |
|
||||
| `brandedHeaderHeight` | Banner height | `48px` |
|
||||
| `brandedHeaderTagline` | Optional tagline text | `Enterprise Platform` |
|
||||
|
||||
### App Bar (Top Navigation)
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `appBarBackgroundColor` | NavBar background |
|
||||
| `appBarTextColor` | NavBar text/icons |
|
||||
|
||||
### Sidebar Navigation
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `sidebarBackgroundColor` | Sidebar background |
|
||||
| `sidebarTextColor` | Sidebar text |
|
||||
| `sidebarIconColor` | Sidebar icons |
|
||||
| `sidebarSelectedBackgroundColor` | Selected item background |
|
||||
| `sidebarSelectedTextColor` | Selected item text |
|
||||
| `sidebarHoverBackgroundColor` | Hover state background |
|
||||
| `sidebarDividerColor` | Section dividers |
|
||||
|
||||
### Data Tables
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `tableHeaderBackgroundColor` | Header row background |
|
||||
| `tableHeaderTextColor` | Header text |
|
||||
| `tableRowHoverColor` | Row hover background |
|
||||
| `tableRowSelectedColor` | Selected row background |
|
||||
| `tableBorderColor` | Table borders |
|
||||
|
||||
### General UI Elements
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `dividerColor` | Horizontal/vertical dividers |
|
||||
| `borderColor` | Input/form borders |
|
||||
| `cardBorderColor` | Card borders |
|
||||
|
||||
### Form Inputs
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `inputBorderColor` | Input field border color |
|
||||
|
||||
### Custom CSS
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|---------|
|
||||
| `customCss` | Raw CSS injection for advanced customization |
|
||||
|
||||
---
|
||||
|
||||
## CSS Variable Reference
|
||||
|
||||
All theme properties become CSS custom properties with `--qqq-` prefix using kebab-case:
|
||||
|
||||
| Java Property | CSS Variable |
|
||||
|--------------|--------------|
|
||||
| `primaryColor` | `--qqq-primary-color` |
|
||||
| `sidebarBackgroundColor` | `--qqq-sidebar-background-color` |
|
||||
| `typographyH1FontSize` | `--qqq-typography-h1-font-size` |
|
||||
|
||||
**Pattern:** `camelCase` → `--qqq-kebab-case`
|
||||
|
||||
### Additional Generated Variables
|
||||
|
||||
The frontend also generates these derived variables:
|
||||
|
||||
```css
|
||||
/* Grey scale */
|
||||
--qqq-grey-100 through --qqq-grey-900
|
||||
|
||||
/* Chart colors */
|
||||
--qqq-chart-grid-color
|
||||
--qqq-chart-text-color
|
||||
--qqq-chart-background-color
|
||||
|
||||
/* Link color (derived from primaryColor) */
|
||||
--qqq-link-color
|
||||
|
||||
/* Spacing (derived from density) */
|
||||
--qqq-spacing-base
|
||||
--qqq-spacing-small
|
||||
--qqq-spacing-medium
|
||||
--qqq-spacing-large
|
||||
|
||||
/* Action colors */
|
||||
--qqq-action-active
|
||||
--qqq-action-hover
|
||||
--qqq-action-selected
|
||||
--qqq-action-disabled
|
||||
|
||||
/* Status variants (light/dark derived from status colors) */
|
||||
--qqq-info-light, --qqq-info-dark
|
||||
--qqq-success-light, --qqq-success-dark
|
||||
--qqq-warning-light, --qqq-warning-dark
|
||||
--qqq-error-light, --qqq-error-dark
|
||||
|
||||
/* Primary contrast text */
|
||||
--qqq-primary-contrast-text
|
||||
```
|
||||
|
||||
### Component-Specific Variables
|
||||
|
||||
These variables are used by components with fallback values. Override them via `customCss`:
|
||||
|
||||
```css
|
||||
/* Stepper */
|
||||
--qqq-stepper-inactive-color: #9fc9ff;
|
||||
|
||||
/* Tooltip */
|
||||
--qqq-tooltip-background-color: #ffffff;
|
||||
--qqq-tooltip-text-color: var(--qqq-text-primary, #344767);
|
||||
--qqq-tooltip-shadow: 0px 0px 12px rgba(128, 128, 128, 0.40);
|
||||
|
||||
/* Form Inputs */
|
||||
--qqq-input-border-color: #d2d6da;
|
||||
|
||||
/* Menu */
|
||||
--qqq-menu-hover-color: #f0f2f5;
|
||||
|
||||
/* Switch */
|
||||
--qqq-switch-track-color: #bdbdbd;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using CSS Variables in Components
|
||||
|
||||
Frontend components use CSS variables with fallbacks:
|
||||
|
||||
```css
|
||||
background-color: var(--qqq-primary-color, #1976D2);
|
||||
color: var(--qqq-sidebar-text-color, #A2A3B7);
|
||||
```
|
||||
|
||||
The fallback value (after comma) is used if the variable is undefined.
|
||||
|
||||
---
|
||||
|
||||
## Theme Application Flow
|
||||
|
||||
1. Backend serves `QThemeMetaData` via `/metaData` endpoint
|
||||
2. Frontend receives theme in `QInstance.theme`
|
||||
3. `themeUtils.ts` calls `injectThemeVariables(theme)`
|
||||
4. Function sets CSS custom properties on `:root`
|
||||
5. Components read values via `var(--qqq-*)` references
|
||||
6. Icon fonts are dynamically loaded based on `iconStyle`
|
||||
|
||||
---
|
||||
|
||||
## Minimal vs Complete Configuration
|
||||
|
||||
**Minimal** - Override only what differs from defaults:
|
||||
```java
|
||||
new QThemeMetaData()
|
||||
.withPrimaryColor("#E91E63")
|
||||
.withBrandedHeaderEnabled(true);
|
||||
```
|
||||
|
||||
**Complete** - Specify all properties for full control (see `EXAMPLE_THEME_CONFIGURATION.md`).
|
||||
|
||||
---
|
||||
|
||||
## Dark Theme Considerations
|
||||
|
||||
For dark themes, invert the typical light/dark relationships:
|
||||
- `backgroundColor`: Dark (e.g., `#121212`)
|
||||
- `surfaceColor`: Slightly lighter dark (e.g., `#1E1E1E`)
|
||||
- `textPrimary`: Light (e.g., `#FFFFFF`)
|
||||
- `primaryColor`: Use lighter/brighter variant for visibility
|
||||
- Adjust sidebar, table, and border colors accordingly
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
| Repository | Key Files |
|
||||
|------------|-----------|
|
||||
| qqq-backend-core | `QThemeMetaData.java` |
|
||||
| qqq-frontend-core | `QThemeMetaData.ts` |
|
||||
| qqq-frontend-material-dashboard | `themeUtils.ts`, `qqq-override-styles.css` |
|
||||
|
||||
---
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- All properties are optional
|
||||
- Missing properties use sensible defaults
|
||||
- Existing applications without theme configuration continue to work unchanged
|
||||
@@ -0,0 +1,78 @@
|
||||
# Session State - Pluggable Themes + CSS Selectors
|
||||
|
||||
**Last Updated:** 2026-01-06
|
||||
**Branch:** `feature/pluggable-themes-v2-mui-css-selectors`
|
||||
**Version:** `0.36.0-pluggable-themes-css-selectors-SNAPSHOT`
|
||||
**Published Tag:** `publish-9035122`
|
||||
|
||||
## Current Status
|
||||
|
||||
Feature snapshot published. Core theming work complete. Identified ~16 files with hardcoded colors that could be converted to CSS variables (optional follow-up work documented in `docs/TODO-hardcoded-colors.md`).
|
||||
|
||||
**Latest commits:**
|
||||
- `9035122` - docs: add TODO for hardcoded color conversions
|
||||
- `cc60a98` - docs: update session state and TODO after JAR verification
|
||||
- `349cf76` - feat(theme): add CSS variable theming to process screens
|
||||
- `1e80deb` - fix(theme): consume sidebarIconColor and fontSizeBase CSS variables
|
||||
|
||||
## To Continue
|
||||
|
||||
1. **Optional:** Convert remaining hardcoded colors per `docs/TODO-hardcoded-colors.md`
|
||||
2. **When ready:** Create PR to develop, update version to `0.36.0-SNAPSHOT` before merge
|
||||
|
||||
## Key Files
|
||||
|
||||
### New Files (This Feature)
|
||||
- `src/qqq/utils/qqqIdUtils.ts` - ID sanitization utilities
|
||||
- `src/qqq/context/QqqIdContext.tsx` - Optional scope context provider
|
||||
- `docs/QQQ_CSS_SELECTORS_GUIDE.md` - Complete selector reference
|
||||
- `docs/PLAN-css-selectors.md` - Implementation plan (complete)
|
||||
|
||||
### Components with Selectors
|
||||
- `src/qqq/pages/records/view/RecordView.tsx` - View screen
|
||||
- `src/qqq/components/forms/EntityForm.tsx` - Create/edit screens
|
||||
- `src/qqq/components/misc/RecordSidebar.tsx` - Section sidebar
|
||||
- `src/qqq/components/legacy/MDButton/*` - Buttons
|
||||
- `src/qqq/components/horseshoe/sidenav/*` - Navigation
|
||||
- `src/qqq/components/forms/DynamicFormField.tsx` - Inputs
|
||||
- `src/qqq/components/forms/DynamicSelect.tsx` - Selects
|
||||
- `src/qqq/pages/records/query/RecordQuery.tsx` - Query screen
|
||||
- `src/qqq/pages/apps/Home.tsx` - App home
|
||||
- `src/qqq/utils/DataGridUtils.tsx` - Table headers
|
||||
|
||||
## CSS Selector Coverage
|
||||
|
||||
| Screen | Elements with Selectors |
|
||||
|--------|------------------------|
|
||||
| **Sidenav** | Root, items, collapse groups, logout button |
|
||||
| **App Home** | App headers, section cards, table/process/report cards |
|
||||
| **Record Query** | Action menu, views button, columns button, table headers |
|
||||
| **Record View** | Header, avatar, title, actions menu, menu items, sections, button bar, delete dialog |
|
||||
| **Record Create** | Header, avatar, title, sections, button bar |
|
||||
| **Record Edit** | Header, avatar, title, sections, button bar |
|
||||
| **Forms** | All inputs, selects, field containers |
|
||||
|
||||
## Selector Patterns
|
||||
|
||||
| Element | Pattern | Example |
|
||||
|---------|---------|---------|
|
||||
| Buttons | `button-{text}` | `button-save` |
|
||||
| Icon buttons | `button-icon-{icon}` | `button-icon-delete` |
|
||||
| Menu items | `menu-item-{text}` | `menu-item-bulk-edit` |
|
||||
| Nav items | `sidenav-{name}` | `sidenav-orders` |
|
||||
| Inputs | `input-{field}` | `input-first-name` |
|
||||
| Selects | `select-{field}` | `select-status` |
|
||||
| Table headers | `table-header-{field}` | `table-header-order-id` |
|
||||
| Record view | `record-view-{element}-{table}` | `record-view-header-orders` |
|
||||
| Record create | `record-create-{element}-{table}` | `record-create-title-person` |
|
||||
| Record edit | `record-edit-{element}-{table}` | `record-edit-avatar-customer` |
|
||||
| Sections | `record-section-{name}` | `record-section-identity` |
|
||||
| Form sections | `form-section-{name}` | `form-section-details` |
|
||||
| Sidebar items | `sidebar-item-{name}` | `sidebar-item-identity` |
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
npm run build # Build frontend
|
||||
mvn clean install -DskipTests # Install snapshot locally
|
||||
```
|
||||
@@ -0,0 +1,206 @@
|
||||
# QQQ Pluggable Theme System - Testing Guide
|
||||
|
||||
**Version:** 0.36.0-SNAPSHOT
|
||||
**Date:** 2026-01-03
|
||||
|
||||
## Overview
|
||||
|
||||
The QQQ dashboard now supports runtime theme customization via CSS custom properties. Theme configuration is defined in Java and automatically injected as `--qqq-*` CSS variables.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>0.36.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
Add theme configuration to your QInstance:
|
||||
|
||||
```java
|
||||
QThemeMetaData theme = new QThemeMetaData()
|
||||
// Core colors
|
||||
.withPrimaryColor("#1976D2")
|
||||
.withSecondaryColor("#9C27B0")
|
||||
.withBackgroundColor("#F5F5F5")
|
||||
.withSurfaceColor("#FFFFFF")
|
||||
.withTextPrimary("#212121")
|
||||
.withTextSecondary("#757575")
|
||||
|
||||
// Status colors
|
||||
.withErrorColor("#D32F2F")
|
||||
.withWarningColor("#F57C00")
|
||||
.withSuccessColor("#388E3C")
|
||||
.withInfoColor("#1976D2")
|
||||
|
||||
// Sidebar
|
||||
.withSidebarBackgroundColor("#1E1E2D")
|
||||
.withSidebarTextColor("#FFFFFF")
|
||||
.withSidebarIconColor("#FFFFFF")
|
||||
|
||||
// Tables
|
||||
.withTableHeaderBackgroundColor("#F5F5F5")
|
||||
.withTableHeaderTextColor("#212121")
|
||||
|
||||
// Typography
|
||||
.withFontFamily("Inter, sans-serif")
|
||||
.withTypographyH1FontSize("2.5rem")
|
||||
.withTypographyH1FontWeight(700)
|
||||
|
||||
// Sizing
|
||||
.withBorderRadius("8px")
|
||||
.withDensity("comfortable");
|
||||
|
||||
qInstance.withSupplementalMetaData(theme);
|
||||
```
|
||||
|
||||
## Available Properties
|
||||
|
||||
### Core Colors
|
||||
| Property | CSS Variable | Default |
|
||||
|----------|--------------|---------|
|
||||
| `primaryColor` | `--qqq-primary-color` | `#0062FF` |
|
||||
| `secondaryColor` | `--qqq-secondary-color` | `#7b809a` |
|
||||
| `backgroundColor` | `--qqq-background-color` | `#f0f2f5` |
|
||||
| `surfaceColor` | `--qqq-surface-color` | `#ffffff` |
|
||||
| `textPrimary` | `--qqq-text-primary` | `#344767` |
|
||||
| `textSecondary` | `--qqq-text-secondary` | `#7b809a` |
|
||||
|
||||
### Status Colors
|
||||
| Property | CSS Variable | Default |
|
||||
|----------|--------------|---------|
|
||||
| `errorColor` | `--qqq-error-color` | `#F44335` |
|
||||
| `warningColor` | `--qqq-warning-color` | `#fb8c00` |
|
||||
| `successColor` | `--qqq-success-color` | `#4CAF50` |
|
||||
| `infoColor` | `--qqq-info-color` | `#0062FF` |
|
||||
|
||||
### Sidebar
|
||||
| Property | CSS Variable |
|
||||
|----------|--------------|
|
||||
| `sidebarBackgroundColor` | `--qqq-sidebar-background-color` |
|
||||
| `sidebarTextColor` | `--qqq-sidebar-text-color` |
|
||||
| `sidebarIconColor` | `--qqq-sidebar-icon-color` |
|
||||
| `sidebarSelectedBackgroundColor` | `--qqq-sidebar-selected-background-color` |
|
||||
| `sidebarSelectedTextColor` | `--qqq-sidebar-selected-text-color` |
|
||||
| `sidebarHoverBackgroundColor` | `--qqq-sidebar-hover-background-color` |
|
||||
| `sidebarDividerColor` | `--qqq-sidebar-divider-color` |
|
||||
|
||||
### Tables
|
||||
| Property | CSS Variable |
|
||||
|----------|--------------|
|
||||
| `tableHeaderBackgroundColor` | `--qqq-table-header-background-color` |
|
||||
| `tableHeaderTextColor` | `--qqq-table-header-text-color` |
|
||||
| `tableRowHoverColor` | `--qqq-table-row-hover-color` |
|
||||
| `tableRowSelectedColor` | `--qqq-table-row-selected-color` |
|
||||
| `tableBorderColor` | `--qqq-table-border-color` |
|
||||
|
||||
### Typography
|
||||
| Property | CSS Variable |
|
||||
|----------|--------------|
|
||||
| `fontFamily` | `--qqq-font-family` |
|
||||
| `headerFontFamily` | `--qqq-header-font-family` |
|
||||
| `monoFontFamily` | `--qqq-mono-font-family` |
|
||||
| `typographyH1FontSize` | `--qqq-typography-h1-font-size` |
|
||||
| `typographyH1FontWeight` | `--qqq-typography-h1-font-weight` |
|
||||
| `typographyH1LineHeight` | `--qqq-typography-h1-line-height` |
|
||||
| `typographyH1LetterSpacing` | `--qqq-typography-h1-letter-spacing` |
|
||||
|
||||
Same pattern for H2-H6, Body1, Body2, Button, Caption.
|
||||
|
||||
### General
|
||||
| Property | CSS Variable | Values |
|
||||
|----------|--------------|--------|
|
||||
| `borderRadius` | `--qqq-border-radius` | CSS size (e.g., `8px`) |
|
||||
| `density` | `--qqq-density` | `compact`, `normal`, `comfortable` |
|
||||
| `dividerColor` | `--qqq-divider-color` | Hex color |
|
||||
| `borderColor` | `--qqq-border-color` | Hex color |
|
||||
| `cardBorderColor` | `--qqq-card-border-color` | Hex color |
|
||||
|
||||
### Branded Header
|
||||
| Property | CSS Variable |
|
||||
|----------|--------------|
|
||||
| `brandedHeaderEnabled` | `--qqq-branded-header-enabled` |
|
||||
| `brandedHeaderBackgroundColor` | `--qqq-branded-header-background-color` |
|
||||
| `brandedHeaderTextColor` | `--qqq-branded-header-text-color` |
|
||||
| `brandedHeaderLogoPath` | `--qqq-branded-header-logo-path` |
|
||||
| `brandedHeaderLogoAltText` | N/A (used in component) |
|
||||
| `brandedHeaderHeight` | `--qqq-branded-header-height` |
|
||||
| `brandedHeaderTagline` | N/A (used in component) |
|
||||
|
||||
### Assets & Icons
|
||||
| Property | Values |
|
||||
|----------|--------|
|
||||
| `logoPath` | URL to logo image |
|
||||
| `iconPath` | URL to icon |
|
||||
| `faviconPath` | URL to favicon |
|
||||
| `iconStyle` | `filled`, `outlined`, `rounded`, `sharp`, `two-tone` |
|
||||
| `customCss` | Raw CSS string injected into page |
|
||||
|
||||
## Test Cases
|
||||
|
||||
### 1. Basic Color Test
|
||||
```java
|
||||
new QThemeMetaData()
|
||||
.withPrimaryColor("#FF5722")
|
||||
.withSidebarBackgroundColor("#263238");
|
||||
```
|
||||
**Verify:** Primary buttons are orange, sidebar is dark blue-gray.
|
||||
|
||||
### 2. Typography Test
|
||||
```java
|
||||
new QThemeMetaData()
|
||||
.withFontFamily("Georgia, serif")
|
||||
.withTypographyH1FontSize("3rem")
|
||||
.withTypographyH1FontWeight(800);
|
||||
```
|
||||
**Verify:** Page uses serif font, H1 headers are large and bold.
|
||||
|
||||
### 3. Branded Header Test
|
||||
```java
|
||||
new QThemeMetaData()
|
||||
.withBrandedHeaderEnabled(true)
|
||||
.withBrandedHeaderBackgroundColor("#1a237e")
|
||||
.withBrandedHeaderTextColor("#ffffff")
|
||||
.withBrandedHeaderTagline("My Application")
|
||||
.withBrandedHeaderHeight("48px");
|
||||
```
|
||||
**Verify:** Fixed header bar appears at top with tagline.
|
||||
|
||||
### 4. Density Test
|
||||
```java
|
||||
new QThemeMetaData()
|
||||
.withDensity("compact");
|
||||
```
|
||||
**Verify:** Reduced spacing throughout the UI.
|
||||
|
||||
### 5. DevTools Verification
|
||||
1. Open browser DevTools
|
||||
2. Inspect `:root` element
|
||||
3. Verify `--qqq-*` CSS variables are present with configured values
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
1. Properties serialize to JSON via `/metaData` endpoint under `supplementalMetaData.theme`
|
||||
2. Frontend reads theme from metadata response
|
||||
3. `themeUtils.ts` injects values as CSS custom properties on `:root`
|
||||
4. MUI components and CSS overrides reference these variables
|
||||
5. Unset properties fall back to sensible defaults
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Theme not applying:**
|
||||
- Check `/metaData` response includes `supplementalMetaData.theme`
|
||||
- Verify CSS variables exist on `:root` in DevTools
|
||||
- Clear browser cache and hard refresh
|
||||
|
||||
**Colors look wrong:**
|
||||
- Ensure hex colors include `#` prefix
|
||||
- Verify color format is `#RGB`, `#RRGGBB`, or `#RRGGBBAA`
|
||||
|
||||
**Typography not changing:**
|
||||
- Font family must be available (loaded via Google Fonts or local)
|
||||
- Check `--qqq-font-family` variable is set correctly
|
||||
@@ -0,0 +1,192 @@
|
||||
# TODO: Convert Hardcoded Colors to CSS Variables
|
||||
|
||||
## Priority 1: Component Files (User-Facing)
|
||||
|
||||
### 1. Widget.tsx
|
||||
**File:** `src/qqq/components/widgets/Widget.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 754 | `background: colors.info.main` | `background: "var(--qqq-info-color)"` |
|
||||
| 772 | `background: colors.info.main` | `background: "var(--qqq-info-color)"` |
|
||||
|
||||
Remove `colors` import if no longer used.
|
||||
|
||||
---
|
||||
|
||||
### 2. ChartSubheaderWithData.tsx
|
||||
**File:** `src/qqq/components/widgets/components/ChartSubheaderWithData.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 49 | `const GOOD_COLOR = colors.success.main;` | `const GOOD_COLOR = "var(--qqq-success-color)";` |
|
||||
| 50 | `const BAD_COLOR = colors.error.main;` | `const BAD_COLOR = "var(--qqq-error-color)";` |
|
||||
|
||||
Remove `colors` import if no longer used.
|
||||
|
||||
---
|
||||
|
||||
### 3. FilterUtils.tsx
|
||||
**File:** `src/qqq/utils/qqq/FilterUtils.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 608 | `background: "#0062FF"` | `background: "var(--qqq-info-color)"` |
|
||||
| 609 | `background: "#757575"` | `background: "var(--qqq-grey-600, #757575)"` |
|
||||
| 610 | `background: "#009971"` | `background: "var(--qqq-success-color)"` |
|
||||
|
||||
---
|
||||
|
||||
### 4. HorizontalBarChart.tsx
|
||||
**File:** `src/qqq/components/widgets/charts/barchart/HorizontalBarChart.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 136 | `colors.info.main` | `"var(--qqq-info-color)"` |
|
||||
|
||||
Note: This is inside a chart config - verify CSS vars work in chart.js context.
|
||||
|
||||
---
|
||||
|
||||
### 5. CronUIWidget.tsx
|
||||
**File:** `src/qqq/components/widgets/misc/CronUIWidget.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 861 | `color={colors.error.main}` | `color="var(--qqq-error-color)"` |
|
||||
|
||||
---
|
||||
|
||||
### 6. RowBuilderWidget.tsx
|
||||
**File:** `src/qqq/components/widgets/misc/RowBuilderWidget.tsx`
|
||||
|
||||
| Lines | Current | Change To |
|
||||
|-------|---------|-----------|
|
||||
| 963-966 | `color: colors.error.main` (4 instances) | `color: "var(--qqq-error-color)"` |
|
||||
|
||||
All 4 lines are delete button hover states.
|
||||
|
||||
---
|
||||
|
||||
### 7. ShareModal.tsx
|
||||
**File:** `src/qqq/components/sharing/ShareModal.tsx`
|
||||
|
||||
| Lines | Current | Change To |
|
||||
|-------|---------|-----------|
|
||||
| 482-485 | `color: colors.error.main` (4 instances) | `color: "var(--qqq-error-color)"` |
|
||||
|
||||
All 4 lines are delete button hover states.
|
||||
|
||||
---
|
||||
|
||||
## Priority 2: Bulk Load Components
|
||||
|
||||
### 8. BulkLoadFileMappingField.tsx
|
||||
**File:** `src/qqq/components/processes/BulkLoadFileMappingField.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 64-67 | `color: colors.error.main` (4 instances) | `color: "var(--qqq-error-color)"` |
|
||||
| 313 | `color={colors.warning.main}` | `color="var(--qqq-warning-color)"` |
|
||||
| 319 | `color={colors.error.main}` | `color="var(--qqq-error-color)"` |
|
||||
|
||||
---
|
||||
|
||||
### 9. BulkLoadFileMappingForm.tsx
|
||||
**File:** `src/qqq/components/processes/BulkLoadFileMappingForm.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 274 | `color={colors.error.main}` | `color="var(--qqq-error-color)"` |
|
||||
|
||||
---
|
||||
|
||||
### 10. BulkLoadValueMappingForm.tsx
|
||||
**File:** `src/qqq/components/processes/BulkLoadValueMappingForm.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 220 | `color={colors.error.main}` | `color="var(--qqq-error-color)"` |
|
||||
|
||||
---
|
||||
|
||||
### 11. SavedBulkLoadProfiles.tsx
|
||||
**File:** `src/qqq/components/misc/SavedBulkLoadProfiles.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 634 | `color={colors.success.main}` | `color="var(--qqq-success-color)"` |
|
||||
| 637 | `color={colors.error.main}` | `color="var(--qqq-error-color)"` |
|
||||
|
||||
---
|
||||
|
||||
## Priority 3: Theme Components
|
||||
|
||||
### 12. tabs/index.ts
|
||||
**File:** `src/qqq/assets/theme/components/tabs/index.ts`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 69 | `borderBottomColor: colors.info.main` | `borderBottomColor: "var(--qqq-info-color)"` |
|
||||
|
||||
---
|
||||
|
||||
### 13. flatpickr.ts
|
||||
**File:** `src/qqq/assets/theme/components/flatpickr.ts`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 46 | `background: \`${gradients.info.state} !important\`` | `background: "var(--qqq-info-color) !important"` |
|
||||
|
||||
---
|
||||
|
||||
### 14. switchButton.ts
|
||||
**File:** `src/qqq/assets/theme/components/form/switchButton.ts`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 68 | `backgroundImage: linearGradient(gradients.info.main, gradients.info.state)` | `backgroundImage: "linear-gradient(195deg, var(--qqq-info-color), var(--qqq-info-color-dark, var(--qqq-info-color)))"` |
|
||||
|
||||
---
|
||||
|
||||
## Priority 4: Legacy Components
|
||||
|
||||
### 15. MDBadgeDot/index.tsx
|
||||
**File:** `src/qqq/components/legacy/MDBadgeDot/index.tsx`
|
||||
|
||||
| Lines | Current | Change To |
|
||||
|-------|---------|-----------|
|
||||
| 94 | `"primary": "#e91e63"` | `"primary": "var(--qqq-primary-color, #e91e63)"` |
|
||||
| 97 | `"success": "#4CAF50"` | `"success": "var(--qqq-success-color, #4CAF50)"` |
|
||||
| 98 | `"warning": "#fb8c00"` | `"warning": "var(--qqq-warning-color, #fb8c00)"` |
|
||||
| 99 | `"error": "#F44335"` | `"error": "var(--qqq-error-color, #F44335)"` |
|
||||
|
||||
Note: Line 96 already uses `accentColor` which is correct.
|
||||
|
||||
---
|
||||
|
||||
### 16. MDProgressRoot.tsx
|
||||
**File:** `src/qqq/components/legacy/MDProgress/MDProgressRoot.tsx`
|
||||
|
||||
| Line | Current | Change To |
|
||||
|------|---------|-----------|
|
||||
| 42 | `linearGradient(gradients.info.main, gradients.info.state)` | `"linear-gradient(195deg, var(--qqq-info-color), var(--qqq-info-color-dark, var(--qqq-info-color)))"` |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Priority | Files | Instances |
|
||||
|----------|-------|-----------|
|
||||
| P1: Components | 7 | ~15 |
|
||||
| P2: Bulk Load | 4 | ~10 |
|
||||
| P3: Theme | 3 | ~3 |
|
||||
| P4: Legacy | 2 | ~5 |
|
||||
| **Total** | **16** | **~33** |
|
||||
|
||||
## After Each Change
|
||||
|
||||
1. Remove unused `colors` import if file no longer uses it
|
||||
2. Remove unused `gradients` import if file no longer uses it
|
||||
3. Run `npm run build` to verify no TypeScript errors
|
||||
4. Test visually that colors still appear correctly
|
||||
@@ -0,0 +1,56 @@
|
||||
# TODO - Pluggable Themes + CSS Selectors
|
||||
|
||||
**Last Updated:** 2026-01-06
|
||||
|
||||
## Active Tasks
|
||||
|
||||
- [ ] **Optional: Convert hardcoded colors to CSS variables**
|
||||
- See `docs/TODO-hardcoded-colors.md` for detailed list (~16 files, ~33 instances)
|
||||
- Priority 1: Widget components (user-facing)
|
||||
- Priority 2: Bulk load components
|
||||
- Priority 3: Theme components
|
||||
- Priority 4: Legacy components
|
||||
|
||||
- [ ] **Prepare PR for develop branch**
|
||||
- Update version back to `0.36.0-SNAPSHOT` before merge
|
||||
|
||||
## Recently Completed
|
||||
|
||||
- [x] **Feature snapshot published** - Tag `publish-9035122`
|
||||
- [x] **JAR build verified** - Contains correct JS (`main.2f9c1ea9.js`)
|
||||
|
||||
## Completed
|
||||
|
||||
- [x] **Fix #314**: CSS variables not consumed by components
|
||||
- Fixed `--qqq-sidebar-icon-color` - removed hardcoded colors in SideNavCollapse.ts, SideNavItem.ts
|
||||
- Fixed `--qqq-font-size-base` - added `.MuiTypography-root` to CSS rule
|
||||
- [x] **Process screens theming**: Updated stepper and process components to use CSS variables
|
||||
- Stepper: background, icon, connector, label now use `--qqq-stepper-*` and `--qqq-primary-color`
|
||||
- ProcessRun, ProcessSummaryResults, ValidationReview: borders and backgrounds now themed
|
||||
|
||||
- [x] Phase 1: Theme infrastructure (CSS variables, themeUtils.ts)
|
||||
- [x] Phase 2: MUI component overrides migration
|
||||
- [x] Phase 3: Component CSS variable consumption
|
||||
- [x] Phase 4: Selenium theme tests (60 tests pass)
|
||||
- [x] **CSS Selectors - Sidenav**: Root, items, collapse, logout
|
||||
- [x] **CSS Selectors - Buttons**: MDButton text/icon, menu buttons
|
||||
- [x] **CSS Selectors - Forms**: Inputs, selects, field containers
|
||||
- [x] **CSS Selectors - Query Screen**: Action menu, views, columns, table headers
|
||||
- [x] **CSS Selectors - App Home**: Headers, sections, table/process/report cards
|
||||
- [x] **CSS Selectors - Record View**: Header, avatar, title, actions menu, sections, button bar, delete dialog
|
||||
- [x] **CSS Selectors - Record Create/Edit**: Header, avatar, title, sections, button bar (via EntityForm)
|
||||
- [x] **CSS Selectors - Sidebar**: Container, section items
|
||||
- [x] **Gradient buttons use CSS variables**: MDButtonRoot maps color props to --qqq-* vars
|
||||
- [x] **Icon boxes use CSS variables**: MiniStatisticsCard, ProcessLinkCard use --qqq-info-color
|
||||
|
||||
## Test Status
|
||||
|
||||
| Test Suite | Tests | Status |
|
||||
|------------|-------|--------|
|
||||
| ThemeMuiComponentIT | 27 | PASS |
|
||||
| ThemeIT | 33 | PASS |
|
||||
| **Total** | **60** | **PASS** |
|
||||
|
||||
## Version
|
||||
|
||||
`0.36.0-pluggable-themes-css-selectors-SNAPSHOT`
|
||||
@@ -0,0 +1,70 @@
|
||||
# Lets build pluggable themes. I'm Scared.
|
||||
|
||||
Every enterprise app eventually gets the same request. "Can we make it blue instead of purple?" "Can we put our logo up there?" "The sidebar needs to match our brand guidelines."
|
||||
|
||||
And every time it's grep through the codebase for hex codes, find them in twelve different files, change them, miss one, deploy, customer notices, repeat.
|
||||
|
||||
So let's build a proper theme system.
|
||||
|
||||
The theme should be defined on the backend, in Java, because that's where the rest of the app configuration lives. Tables, processes, reports - all defined in Java metadata. The theme should work the same way.
|
||||
|
||||
```java
|
||||
qInstance.setTheme(new QThemeMetaData()
|
||||
.withPrimaryColor("#1976D2")
|
||||
.withSecondaryColor("#9C27B0")
|
||||
.withSidebarBackgroundColor("#1E1E2D")
|
||||
.withSidebarTextColor("#FFFFFF")
|
||||
.withFontFamily("Inter")
|
||||
.withBrandedHeaderEnabled(true)
|
||||
.withBrandedHeaderTagline("Acme Corp Portal"));
|
||||
```
|
||||
|
||||
Fluent API. Discoverable. Type-safe.
|
||||
|
||||
How does that get to the frontend? Two options. Bake it into the build - generate CSS at compile time. But the frontend is built and packaged upstream as a static SPA. Downstream apps consume it as a dependency. They can't rebuild it with different colors.
|
||||
|
||||
So it has to be runtime. Send the theme with the metadata response. The frontend already fetches `/metaData` on load. Include the theme there. Each consuming app defines its own theme in Java, the frontend picks it up and applies it.
|
||||
|
||||
So the frontend gets a theme object. How do you apply it?
|
||||
|
||||
React context is one option - pass theme values down through providers. But every component needs to consume the context. That's a lot of changes. And it doesn't help with third-party components.
|
||||
|
||||
CSS-in-JS with a theme provider is another option. MUI supports this. But we're already fighting MUI's default styles. More abstraction isn't the answer.
|
||||
|
||||
CSS custom properties though. Set them once at the root:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--qqq-primary-color: #1976D2;
|
||||
--qqq-sidebar-background: #1E1E2D;
|
||||
}
|
||||
```
|
||||
|
||||
Reference them everywhere:
|
||||
|
||||
```css
|
||||
.sidebar {
|
||||
background-color: var(--qqq-sidebar-background);
|
||||
}
|
||||
```
|
||||
|
||||
This cascades naturally. Set the variables, everything that uses them just works. No prop drilling. No context. No build step.
|
||||
|
||||
The frontend reads the theme from metadata, injects variables into the document root. Done.
|
||||
|
||||
The challenge is MUI. It has its own theming system, its own styled-components that inject styles at runtime with high specificity. CSS variables don't automatically win against that. Override styles are needed, with enough specificity to beat MUI's generated classes.
|
||||
|
||||
And there are multiple theming islands. The sidebar has its own colors - background, text, icons, hover states, selected states, dividers. The branded header has its own colors. Data tables have their own. They can't just inherit from global theme variables.
|
||||
|
||||
So we're looking at roughly 60 properties:
|
||||
|
||||
- Core palette: primary, secondary, background, surface, error, warning, success, info
|
||||
- Typography: font family, sizes, weights for different variants
|
||||
- Sidebar: background, text, icons, hover, selected, dividers
|
||||
- Branded header: background, text, logo path, tagline
|
||||
- Tables: header background, header text, row hover, borders
|
||||
- General: border radius, spacing scale, density
|
||||
|
||||
Each one flows from Java to frontend to CSS to rendered component.
|
||||
|
||||
The infrastructure first - theme object flowing from backend to frontend, CSS variable injection. Then components one by one. Buttons, inputs, sidebar, tables.
|
||||
@@ -0,0 +1,59 @@
|
||||
# The Implementation Grind
|
||||
|
||||
First attempt:
|
||||
|
||||
```css
|
||||
.MuiButton-root {
|
||||
background-color: var(--qqq-primary-color);
|
||||
}
|
||||
```
|
||||
|
||||
Doesn't work. MUI generates class names like `.css-1hw9j7s` and injects them with inline-level specificity. Regular stylesheets don't stand a chance.
|
||||
|
||||
`!important` it is:
|
||||
|
||||
```css
|
||||
.MuiButton-containedPrimary {
|
||||
background-color: var(--qqq-primary-color) !important;
|
||||
}
|
||||
```
|
||||
|
||||
Not elegant, but effective. Buttons, inputs, cards - all picking up theme colors now.
|
||||
|
||||
Then a weirder problem. A test for `textSecondary` keeps failing even though the CSS variable is set correctly and the value matches.
|
||||
|
||||
After debugging: the test was finding a `.MuiTypography-body2` element in the sidebar, not the main content. The sidebar has its own text color variable. The test found an element, checked its color, but checked the wrong element.
|
||||
|
||||
The fix:
|
||||
|
||||
```css
|
||||
.MuiTypography-body2:not(.MuiDrawer-root *):not(.MuiDrawer-paper *) {
|
||||
color: var(--qqq-text-secondary) !important;
|
||||
}
|
||||
```
|
||||
|
||||
"Apply this style, but not if inside the drawer."
|
||||
|
||||
Same pattern needed for the branded header - it has its own text color too. So now there are `:not()` selectors everywhere.
|
||||
|
||||
```css
|
||||
.MuiTypography-body2:not(.MuiDrawer-root *):not(.MuiDrawer-paper *):not(.qqq-branded-header-bar *) {
|
||||
color: var(--qqq-text-secondary) !important;
|
||||
}
|
||||
```
|
||||
|
||||
The CSS override file is around 1200 lines now. Organized by component group:
|
||||
|
||||
- Core colors and typography
|
||||
- Sidebar states (default, hover, selected)
|
||||
- Branded header
|
||||
- Data tables (headers, cells, borders)
|
||||
- Form inputs
|
||||
- Cards and surfaces
|
||||
- Buttons and links
|
||||
|
||||
Each section targets the MUI classes for that component type and forces them to use CSS variables.
|
||||
|
||||
What's working: font family and sizes flow through cleanly. Border radius applies everywhere. Table headers picked up the colors without issues. Sidebar selected and hover states work correctly.
|
||||
|
||||
Next: actual browser tests. Playwright, not Jest. Render the page and check `getComputedStyle()`. The tests need to verify what the user actually sees, not just that variables are set.
|
||||
@@ -0,0 +1,136 @@
|
||||
# 26 Tests Green
|
||||
|
||||
All green. Finally.
|
||||
|
||||
Pushed it to CI, watched the pipeline, held my breath... and it passed. 26 Playwright tests, all checking actual rendered colors on actual components. Not mocked. Not faked. Real browser, real CSS, real values.
|
||||
|
||||
Getting here was annoying though.
|
||||
|
||||
My first tests were wrong. Like, fundamentally wrong. They checked if the CSS variable was *set*:
|
||||
|
||||
```typescript
|
||||
const cssVar = getComputedStyle(root).getPropertyValue('--qqq-primary-color');
|
||||
expect(cssVar).toBe('#E91E63');
|
||||
```
|
||||
|
||||
Those passed. Great, right? No. The button on screen was still the wrong color. The variable existed, the component just wasn't using it.
|
||||
|
||||
So I had to rewrite everything to check what actually renders:
|
||||
|
||||
```typescript
|
||||
const btn = document.querySelector('.MuiButton-containedPrimary');
|
||||
const bg = getComputedStyle(btn).backgroundColor;
|
||||
```
|
||||
|
||||
If the button looks wrong, the test fails. That's the whole point.
|
||||
|
||||
## The Testing Architecture
|
||||
|
||||
Ended up with three test classes, each with a different purpose:
|
||||
|
||||
**ThemeIT** - CSS variable injection tests. Does the theme JSON get converted into `--qqq-*` variables? Simple, fast, catches the obvious stuff.
|
||||
|
||||
**ThemeComponentRenderingIT** - Component rendering tests. Given a sidebar with `sidebarBackgroundColor: '#1B5E20'`, does the MuiDrawer actually render green? This is where we catch the "variable exists but nothing uses it" problem.
|
||||
|
||||
**ThemeMuiComponentIT** - Strict MUI component tests. These are the brutal ones. They find the exact MUI element and assert the exact computed style. No fallbacks, no "well close enough."
|
||||
|
||||
```java
|
||||
WebElement drawer = findElementOrFail(
|
||||
By.cssSelector(".MuiDrawer-paper"),
|
||||
"MUI Drawer (sidebar)"
|
||||
);
|
||||
|
||||
String drawerBgColor = getComputedStyle(drawer, "background-color");
|
||||
assertThat(normalizeColorToHex(drawerBgColor))
|
||||
.as("MUI Drawer sidebar MUST render with green background #1B5E20, but was %s", drawerBgColor)
|
||||
.isEqualToIgnoringCase("#1B5E20");
|
||||
```
|
||||
|
||||
No silent skipping. `findElementOrFail` means if the element doesn't exist, the test fails with a clear message. Not "element not found," but "Could not find MUI Drawer (sidebar)."
|
||||
|
||||
## The Two-Server Setup
|
||||
|
||||
Tests run against a real React dev server and a mock Javalin backend. React on port 3001, Javalin on 8001. The mock serves JSON fixtures from `src/test/resources/fixtures/`.
|
||||
|
||||
Different fixtures for different test scenarios:
|
||||
- `withFullCustomTheme.json` - all 60+ properties set
|
||||
- `withCompactDensity.json` - compact spacing values
|
||||
- `withBrandedHeader.json` - branded header enabled
|
||||
- `index.json` - no theme, uses defaults
|
||||
|
||||
Each test picks its fixture, restarts Javalin, loads the page, runs assertions.
|
||||
|
||||
## The Color Comparison Problem
|
||||
|
||||
The annoying part was elements showing up in the wrong containers. I'd query for `.MuiTypography-body2` and find one - but it was in the sidebar, which has its own color scheme. Or in the branded header. Both of which should be *excluded* from the general text color rules.
|
||||
|
||||
So now every test does this dance:
|
||||
|
||||
```typescript
|
||||
for (const el of allElements) {
|
||||
if (el.closest('.MuiDrawer-root')) continue;
|
||||
if (el.closest('.qqq-branded-header-bar')) continue;
|
||||
return getComputedStyle(el).color;
|
||||
}
|
||||
```
|
||||
|
||||
Loop through, skip the containers we don't want, grab the first valid one.
|
||||
|
||||
Oh and there was this one bug that took me way too long. Test output said:
|
||||
|
||||
```
|
||||
expected #CFD8DC, got #CFD8DC
|
||||
```
|
||||
|
||||
Same value. Still failing. What?
|
||||
|
||||
Turns out my color comparison function was trying to parse both as RGB, failing because they were hex, and returning false. I just needed to check if both start with `#` and compare directly. Twenty minutes I'll never get back.
|
||||
|
||||
```java
|
||||
private String normalizeColorToHex(String color)
|
||||
{
|
||||
if(color.startsWith("#"))
|
||||
{
|
||||
return color.toUpperCase();
|
||||
}
|
||||
|
||||
if(color.startsWith("rgb"))
|
||||
{
|
||||
String numbers = color.replaceAll("[^0-9,.]", "");
|
||||
String[] parts = numbers.split(",");
|
||||
int r = Integer.parseInt(parts[0].trim());
|
||||
int g = Integer.parseInt(parts[1].trim());
|
||||
int b = Integer.parseInt(parts[2].trim());
|
||||
return String.format("#%02X%02X%02X", r, g, b);
|
||||
}
|
||||
return color.toUpperCase();
|
||||
}
|
||||
```
|
||||
|
||||
Browsers return computed colors as RGB. Theme fixtures use hex. Need to normalize both to the same format before comparing.
|
||||
|
||||
## What's Not Tested Yet
|
||||
|
||||
Hover and focus states are partially covered. We use JavaScript to dispatch mouse events:
|
||||
|
||||
```java
|
||||
js.executeScript("arguments[0].dispatchEvent(new MouseEvent('mouseenter', {bubbles: true}));", item);
|
||||
```
|
||||
|
||||
But it's flaky. The hover state might not apply before we read the style. There's a `waitForSeconds(1)` in there which is gross. Need a better way to verify CSS transitions complete.
|
||||
|
||||
Visual regression testing would catch things unit tests miss. Compare screenshots across builds, flag pixel differences. Tools like Percy or Chromatic do this. Worth exploring for the theme system specifically.
|
||||
|
||||
Cross-browser testing is another gap. These run in headless Chrome. Safari renders things slightly differently. Firefox has its own quirks. CI should probably run the suite against multiple browsers.
|
||||
|
||||
Accessibility testing for color contrast. If someone sets `textPrimary: '#CCCCCC'` on `backgroundColor: '#EEEEEE'`, that's technically valid but unreadable. Should probably warn about insufficient contrast ratios.
|
||||
|
||||
## Where This Goes Next
|
||||
|
||||
The test suite proves the theme system works. But it's testing qfmd in isolation. Downstream apps like me-health-portal have their own test suites. They pull qfmd as a dependency, apply their own theme, and their tests should verify the theming works in their context.
|
||||
|
||||
Right now other teams are pulling the snapshot to test in their apps. Real usage, real feedback. If something breaks, I'll hear about it.
|
||||
|
||||
Then docs. 60+ theme properties need to be documented somewhere. But that's a problem for next week.
|
||||
|
||||
[43522d6](https://github.com/QRun-IO/qqq-frontend-material-dashboard/commit/43522d6)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Where We Are Now
|
||||
|
||||
The theme system works. 60+ properties flow from Java metadata to CSS variables to rendered components. Tests pass. Other teams are testing in their apps. The feature is real.
|
||||
|
||||
But after living with the code for a few days, I'm not happy with how we got here.
|
||||
|
||||
## What's Working
|
||||
|
||||
The architecture holds up. Define theme in Java, ship it with metadata, inject as CSS variables, components pick it up. No rebuild required. No separate config files. Each tenant can have different colors without touching the frontend codebase.
|
||||
|
||||
```java
|
||||
qInstance.setTheme(new QThemeMetaData()
|
||||
.withPrimaryColor("#1976D2")
|
||||
.withSidebarBackgroundColor("#1E1E2D"));
|
||||
```
|
||||
|
||||
That's it. Two lines and the app looks different.
|
||||
|
||||
The sidebar theming is solid. Background, text, icons, hover, selected states - all wired up. Same with the branded header. Same with data tables. The component "islands" each have their own color namespace and it works.
|
||||
|
||||
## What's Ugly
|
||||
|
||||
The CSS override file. 1200+ lines of specificity battles with MUI.
|
||||
|
||||
```css
|
||||
.MuiButton-containedPrimary {
|
||||
background-color: var(--qqq-primary-color) !important;
|
||||
}
|
||||
```
|
||||
|
||||
`!important` everywhere. 82 of them. That's not engineering, that's brute force.
|
||||
|
||||
The `:not()` chains are getting ridiculous:
|
||||
|
||||
```css
|
||||
.MuiTypography-body2:not(.MuiDrawer-root *):not(.MuiDrawer-paper *):not(.qqq-branded-header-bar *) {
|
||||
color: var(--qqq-text-secondary) !important;
|
||||
}
|
||||
```
|
||||
|
||||
Every new MUI component needs its own override. Every MUI version bump might break something. Every new island component means adding more `:not()` exclusions everywhere.
|
||||
|
||||
The disconnect hit me when I traced the data flow:
|
||||
|
||||
```
|
||||
QThemeMetaData (backend)
|
||||
-> injectThemeVariables() -> CSS variables on :root
|
||||
-> colors.ts reads CSS vars -> Theme.ts builds MUI theme
|
||||
-> ThemeProvider wraps app
|
||||
-> PLUS: 1200 lines of CSS overrides fighting MUI anyway
|
||||
```
|
||||
|
||||
We're already using MUI's ThemeProvider. We're already building a dynamic theme. But then we're overriding everything with CSS because we didn't trust MUI to do its job.
|
||||
|
||||
Two theming systems fighting each other. That's the problem.
|
||||
|
||||
## The Decision
|
||||
|
||||
We're going to refactor.
|
||||
|
||||
The CSS variable approach taught us a lot. We understand the 60+ theme properties. We know which components need which colors. We have 26 Playwright tests that verify the rendered output. That's not wasted work - that's reconnaissance.
|
||||
|
||||
But the implementation needs to change. Instead of CSS variables overriding MUI from the outside, we're going to:
|
||||
|
||||
1. Build the MUI theme directly from QThemeMetaData
|
||||
2. Let MUI style its own components correctly from the start
|
||||
3. Keep CSS variables only for "islands" (sidebar, branded header) and non-MUI components
|
||||
4. Delete most of the 1200-line override file
|
||||
|
||||
```typescript
|
||||
// Before: CSS variables -> colors.ts reads them -> Theme.ts -> overrides anyway
|
||||
// After: QThemeMetaData -> createDynamicTheme() -> MUI handles it
|
||||
|
||||
function createDynamicTheme(themeData: QThemeMetaData): Theme {
|
||||
return createTheme({
|
||||
palette: {
|
||||
primary: { main: themeData.primaryColor },
|
||||
secondary: { main: themeData.secondaryColor },
|
||||
background: { default: themeData.backgroundColor },
|
||||
text: { primary: themeData.textPrimary },
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
containedPrimary: {
|
||||
backgroundColor: themeData.primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Direct data flow. No round-trip through CSS variables. MUI components styled correctly without `!important`. The browser's CSS engine isn't fighting itself.
|
||||
|
||||
## What We Gain
|
||||
|
||||
- **Smaller override file.** 1200 lines -> maybe 200-300 for islands and DataGrid.
|
||||
- **Fewer `!important`.** 82 -> maybe 10-15.
|
||||
- **Easier debugging.** Theme is an inspectable object, not scattered CSS vars.
|
||||
- **MUI version safety.** We're using MUI's API, not fighting its internals.
|
||||
- **Future features.** Dark mode, hot reload, theme switching - all become possible.
|
||||
|
||||
## What's Next
|
||||
|
||||
The 26 Playwright tests stay. They verify rendered colors, not implementation details. When the refactor is done, they should all pass with no changes.
|
||||
|
||||
The CSS variable work moves to a feature branch. It's not getting deleted - it's useful as reference and for the blog posts. But develop goes back to pre-theme state.
|
||||
|
||||
New feature branch for the MUI refactor. Clean implementation, right architecture.
|
||||
|
||||
Sometimes the fastest way forward is to step back and do it right.
|
||||
@@ -0,0 +1,180 @@
|
||||
# The Selector Problem
|
||||
|
||||
The theme system works. Colors flow from Java to CSS variables to rendered components. Ship it, right?
|
||||
|
||||
Then the first real feedback: "How do I hide the Developer Mode menu item?"
|
||||
|
||||
Good question. Check the DOM. The menu item is a `<li>` with class `.MuiMenuItem-root` and some generated hash like `.css-kk1bwy-MuiMenuItem-root`. No stable identifier. No way to target just that one menu item.
|
||||
|
||||
"Use the text content?"
|
||||
|
||||
```css
|
||||
/* This doesn't exist in CSS */
|
||||
.MuiMenuItem-root:contains("Developer Mode") {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
CSS doesn't have a `:contains()` selector. JavaScript does, but we're trying to enable customization through `customCss` in `QThemeMetaData`. Pure CSS. No JS allowed.
|
||||
|
||||
The customer could override the whole menu. Or hide all menu items and recreate them. But that's insane for "hide one button."
|
||||
|
||||
## The MUI Class Name Problem
|
||||
|
||||
MUI generates class names at build time. They're deterministic but meaningless:
|
||||
|
||||
```html
|
||||
<button class="MuiButton-root MuiButton-containedPrimary css-1hw9j7s">
|
||||
Save
|
||||
</button>
|
||||
```
|
||||
|
||||
That `css-1hw9j7s` changes if MUI's internals change. Can't rely on it. The `MuiButton-*` classes are stable but they're type selectors, not instance selectors. Every primary button has the same classes.
|
||||
|
||||
Checked if MUI has built-in solutions. It does have a `className` generator you can configure. Doesn't help - it changes the hash algorithm, not the semantic meaning. Still no way to say "this specific button."
|
||||
|
||||
There's no built-in "give every element a unique stable ID" feature. MUI expects you to know which button you're styling because you wrote the code. Makes sense for apps, breaks down for configurable frameworks.
|
||||
|
||||
## Data Attributes
|
||||
|
||||
The answer is `data-*` attributes. HTML's escape hatch for custom metadata.
|
||||
|
||||
```html
|
||||
<button data-qqq-id="button-save" class="MuiButton-root ...">
|
||||
Save
|
||||
</button>
|
||||
```
|
||||
|
||||
Now CSS can target it:
|
||||
|
||||
```css
|
||||
[data-qqq-id="button-save"] {
|
||||
background-color: green !important;
|
||||
}
|
||||
```
|
||||
|
||||
Stable. Semantic. Survives MUI upgrades. Works in `customCss`.
|
||||
|
||||
## Auto-Generation
|
||||
|
||||
Could require developers to manually add `qqqId` props everywhere. But that's tedious and error-prone. Most elements have obvious identifiers already - button text, field names, menu item labels.
|
||||
|
||||
So: auto-generate from what's already there.
|
||||
|
||||
```typescript
|
||||
function generateButtonId(qqqId?: string, children?: ReactNode, iconName?: string): string {
|
||||
if (qqqId) return `button-${sanitizeId(qqqId)}`;
|
||||
|
||||
const text = extractTextFromChildren(children);
|
||||
if (text) return `button-${sanitizeId(text)}`;
|
||||
|
||||
if (iconName) return `button-icon-${sanitizeId(iconName)}`;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
```
|
||||
|
||||
A "Save" button becomes `button-save`. An icon-only edit button becomes `button-icon-edit`. Explicit `qqqId` prop overrides everything.
|
||||
|
||||
Same pattern for inputs (from field name), selects (from field name), nav items (from route), menu items (from text), table headers (from column field).
|
||||
|
||||
The sanitizer strips everything down to lowercase alphanumeric with dashes:
|
||||
|
||||
```typescript
|
||||
function sanitizeId(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.substring(0, 50);
|
||||
}
|
||||
```
|
||||
|
||||
"Bulk Edit With File" becomes `bulk-edit-with-file`. Predictable. Debuggable.
|
||||
|
||||
## The Implementation
|
||||
|
||||
Modified eight components:
|
||||
|
||||
- `MDButton` - core button, auto-generates from children or icon
|
||||
- `DefaultButtons` - standard buttons get explicit IDs (`button-save`, `button-cancel`, etc.)
|
||||
- `SideNavItem` - nav items from `name` prop
|
||||
- `QueryScreenActionMenu` - all menu items from text
|
||||
- `DynamicFormField` - text inputs from field name
|
||||
- `DynamicSelect` - dropdowns from field name
|
||||
- `DataGridUtils` - table headers from column field
|
||||
|
||||
Created utility file `qqqIdUtils.ts` with generators for each element type. Created `QqqIdContext.tsx` for optional page-level scoping (not used yet, but there for future).
|
||||
|
||||
Total: ~200 lines of new code, ~50 lines of modifications across existing components.
|
||||
|
||||
## What You Can Do Now
|
||||
|
||||
Hide a menu item:
|
||||
|
||||
```java
|
||||
.withCustomCss("""
|
||||
[data-qqq-id="menu-item-developer-mode"] {
|
||||
display: none;
|
||||
}
|
||||
""")
|
||||
```
|
||||
|
||||
Style a specific button:
|
||||
|
||||
```css
|
||||
[data-qqq-id="button-save"] {
|
||||
background-color: #4CAF50 !important;
|
||||
}
|
||||
```
|
||||
|
||||
Target inputs by pattern:
|
||||
|
||||
```css
|
||||
[data-qqq-id^="input-order"] {
|
||||
border-left: 3px solid blue;
|
||||
}
|
||||
```
|
||||
|
||||
Hide a nav item:
|
||||
|
||||
```css
|
||||
[data-qqq-id="sidenav-admin"] {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
Style table headers:
|
||||
|
||||
```css
|
||||
[data-qqq-id="table-header-status"] {
|
||||
font-weight: bold;
|
||||
background-color: #FFF3E0;
|
||||
}
|
||||
```
|
||||
|
||||
Inspect any element in DevTools, find its `data-qqq-id`, target it in CSS. That simple.
|
||||
|
||||
## What's Not Covered Yet
|
||||
|
||||
Tabs aren't wired up. Neither are cards, modals, or the dozens of other component types. The pattern is established though - adding more is mechanical.
|
||||
|
||||
No tests yet for the selectors themselves. The theme tests verify colors render correctly; should add tests that verify IDs appear on elements.
|
||||
|
||||
The scoping context exists but isn't used. Idea is to wrap pages in `<QqqIdProvider scope="record-view">` so you can write:
|
||||
|
||||
```css
|
||||
[data-qqq-scope="record-view"] [data-qqq-id="button-edit"] {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
Hide the edit button only on view screens, not everywhere. That's future work.
|
||||
|
||||
## The Pattern
|
||||
|
||||
This keeps happening. Build a feature, ship it, get feedback that reveals a gap in the abstraction. Theme colors work but you can't target specific elements. Fix that, ship it, wait for the next gap.
|
||||
|
||||
The architecture is holding up though. Java metadata → frontend → CSS → rendered output. Each layer adds capability without breaking the ones below.
|
||||
|
||||
Version for testing: `0.36.0-pluggable-themes-css-selectors-SNAPSHOT`
|
||||
Generated
+35
-53
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qqq-frontend-material-dashboard",
|
||||
"version": "0.28.3-SNAPSHOT",
|
||||
"version": "0.36.0-SNAPSHOT",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qqq-frontend-material-dashboard",
|
||||
"version": "0.28.3-SNAPSHOT",
|
||||
"version": "0.36.0-SNAPSHOT",
|
||||
"dependencies": {
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@mui/x-data-grid-pro": "5.17.23",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-license-pro": "5.12.3",
|
||||
"@qrunio/qqq-frontend-core": "1.3.7-SNAPSHOT",
|
||||
"@qrunio/qqq-frontend-core": "1.4.2-SNAPSHOT",
|
||||
"@react-jvectormap/core": "1.0.1",
|
||||
"@react-jvectormap/unitedstates": "1.0.1",
|
||||
"@react-oauth/google": "0.2.8",
|
||||
@@ -166,7 +166,7 @@
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
|
||||
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -176,7 +176,7 @@
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -207,7 +207,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/eslint-parser": {
|
||||
@@ -272,7 +272,7 @@
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
@@ -382,7 +382,7 @@
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
|
||||
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
@@ -491,7 +491,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -516,7 +516,7 @@
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
|
||||
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
@@ -3243,7 +3243,7 @@
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -4017,9 +4017,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@qrunio/qqq-frontend-core": {
|
||||
"version": "1.3.7-SNAPSHOT",
|
||||
"resolved": "https://registry.npmjs.org/@qrunio/qqq-frontend-core/-/qqq-frontend-core-1.3.7-SNAPSHOT.tgz",
|
||||
"integrity": "sha512-Y59lQBAvElzv4lazDSBPfXgdVWWrrHPza+1ih/kVNRr5x9MzDS10K4SVd4RMCmhlrwwhXWK4B4IT5MXzTi+KtA==",
|
||||
"version": "1.4.2-SNAPSHOT",
|
||||
"resolved": "https://registry.npmjs.org/@qrunio/qqq-frontend-core/-/qqq-frontend-core-1.4.2-SNAPSHOT.tgz",
|
||||
"integrity": "sha512-yPnWsfK+K5E8yZks70Nif6xHmN+DyI21r5D5n7FgdXBcwMRyeEkV/HiTli1IJfP4q6CY+TGMQNp4oD1fsoUs0A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "0.28.1",
|
||||
@@ -5464,7 +5464,7 @@
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -5492,7 +5492,7 @@
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -5565,7 +5565,7 @@
|
||||
"version": "4.17.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
|
||||
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -5578,7 +5578,7 @@
|
||||
"version": "4.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
|
||||
"integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -5648,7 +5648,7 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
@@ -5720,7 +5720,7 @@
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
@@ -5771,7 +5771,7 @@
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ramda": {
|
||||
@@ -5787,7 +5787,7 @@
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
@@ -5895,7 +5895,7 @@
|
||||
"version": "0.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
||||
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
@@ -5916,7 +5916,7 @@
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
||||
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -7448,7 +7448,7 @@
|
||||
"version": "2.8.32",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz",
|
||||
"integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -7624,7 +7624,7 @@
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
|
||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7827,7 +7827,7 @@
|
||||
"version": "1.0.30001757",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -9457,7 +9457,7 @@
|
||||
"version": "1.5.262",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
|
||||
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
@@ -11452,7 +11452,7 @@
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -14409,7 +14409,7 @@
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -14868,7 +14868,7 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
@@ -15912,7 +15912,7 @@
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -19686,7 +19686,7 @@
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -21714,7 +21714,7 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -22795,26 +22795,8 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "qqq-frontend-material-dashboard",
|
||||
"version": "0.28.3-SNAPSHOT",
|
||||
"version": "0.36.0-SNAPSHOT",
|
||||
"homepage": ".",
|
||||
"description": "QQQ Default Dashboard",
|
||||
"dependencies": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"@mui/x-data-grid-pro": "5.17.23",
|
||||
"@mui/x-date-pickers": "7.1.1",
|
||||
"@mui/x-license-pro": "5.12.3",
|
||||
"@qrunio/qqq-frontend-core": "1.3.7-SNAPSHOT",
|
||||
"@qrunio/qqq-frontend-core": "1.4.2-SNAPSHOT",
|
||||
"@react-jvectormap/core": "1.0.1",
|
||||
"@react-jvectormap/unitedstates": "1.0.1",
|
||||
"@react-oauth/google": "0.2.8",
|
||||
|
||||
+36
-3
@@ -27,6 +27,9 @@ import {QBrandingMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QB
|
||||
import {QInstance} from "@qrunio/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QProcessMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||
import {QTableMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {QThemeMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
import {Theme} from "@mui/material";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
@@ -38,8 +41,10 @@ import QContext from "QContext";
|
||||
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||
import BrandedHeaderBar from "qqq/components/horseshoe/BrandedHeaderBar";
|
||||
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
||||
import theme from "qqq/components/legacy/Theme";
|
||||
import {createDynamicTheme} from "qqq/utils/createDynamicTheme";
|
||||
import {injectIslandVariables} from "qqq/utils/injectIslandVariables";
|
||||
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
|
||||
import AppHome from "qqq/pages/apps/Home";
|
||||
@@ -57,7 +62,7 @@ import RecordViewByUniqueKey from "qqq/pages/records/view/RecordViewByUniqueKey"
|
||||
import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
||||
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useMemo, useState,} from "react";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom";
|
||||
import {Md5} from "ts-md5/dist/md5";
|
||||
@@ -78,6 +83,7 @@ export default function App({authenticationMetaData}: Props)
|
||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||
const [profileRoutes, setProfileRoutes] = useState({});
|
||||
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
||||
const [themeMetaData, setThemeMetaData] = useState(null as QThemeMetaData | null);
|
||||
const [metaData, setMetaData] = useState({} as QInstance);
|
||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||
@@ -490,6 +496,14 @@ export default function App({authenticationMetaData}: Props)
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// Set theme metadata for dynamic theming //
|
||||
///////////////////////////////////////////////
|
||||
if (metaData.theme)
|
||||
{
|
||||
setThemeMetaData(metaData.theme);
|
||||
}
|
||||
|
||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||
const profilePicture = `${gravatarBase}${hash}`;
|
||||
@@ -633,6 +647,24 @@ export default function App({authenticationMetaData}: Props)
|
||||
},
|
||||
);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Create dynamic MUI theme from themeMetaData //
|
||||
// Theme is rebuilt when themeMetaData changes (from backend) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const dynamicTheme: Theme = useMemo(() =>
|
||||
{
|
||||
return createDynamicTheme(themeMetaData || undefined);
|
||||
}, [themeMetaData]);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Inject CSS variables for island components (sidebar, header, DataGrid) //
|
||||
// These components can't use MUI palette, so they need CSS variables //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
injectIslandVariables(themeMetaData || undefined);
|
||||
}, [themeMetaData]);
|
||||
|
||||
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
|
||||
const [accentColor, setAccentColor] = useState("#0062FF");
|
||||
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7");
|
||||
@@ -754,10 +786,11 @@ export default function App({authenticationMetaData}: Props)
|
||||
pathToLabelMap: pathToLabelMap,
|
||||
branding: branding
|
||||
}}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={dynamicTheme}>
|
||||
<CssBaseline />
|
||||
<CommandMenu metaData={metaData} />
|
||||
{banner()}
|
||||
<BrandedHeaderBar theme={themeMetaData} />
|
||||
<Sidenav
|
||||
color={sidenavColor}
|
||||
icon={resolveAssetUrl(branding.icon)}
|
||||
|
||||
@@ -16,15 +16,12 @@ Coded by www.creative-tim.com
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import borders from "qqq/assets/theme/base/borders";
|
||||
import boxShadows from "qqq/assets/theme/base/boxShadows";
|
||||
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
import linearGradient from "qqq/assets/theme/functions/linearGradient";
|
||||
|
||||
const { transparent, gradients } = colors;
|
||||
const { transparent } = colors;
|
||||
const { borderRadius } = borders;
|
||||
const { colored } = boxShadows;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
@@ -32,10 +29,10 @@ type Types = any;
|
||||
const stepper: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
background: linearGradient(gradients.info.main, gradients.info.state),
|
||||
background: "var(--qqq-stepper-background, linear-gradient(195deg, var(--qqq-primary-color), var(--qqq-primary-color-dark, var(--qqq-primary-color))))",
|
||||
padding: `${pxToRem(24)} 0 ${pxToRem(16)}`,
|
||||
borderRadius: borderRadius.lg,
|
||||
boxShadow: colored.info,
|
||||
boxShadow: "var(--qqq-stepper-shadow, 0rem 0.25rem 1.25rem 0rem rgba(0, 0, 0, 0.14), 0rem 0.4375rem 0.625rem -0.3125rem rgba(0, 0, 0, 0.4))",
|
||||
|
||||
"&.MuiPaper-root": {
|
||||
backgroundColor: transparent.main,
|
||||
|
||||
@@ -15,9 +15,7 @@ Coded by www.creative-tim.com
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import borders from "qqq/assets/theme/base/borders";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
const { white } = colors;
|
||||
const { borderWidth } = borders;
|
||||
|
||||
// types
|
||||
@@ -26,15 +24,15 @@ type Types = any;
|
||||
const stepConnector: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: "#9fc9ff",
|
||||
color: "var(--qqq-stepper-inactive-color, rgba(255, 255, 255, 0.5))",
|
||||
transition: "all 200ms linear",
|
||||
|
||||
"&.Mui-active": {
|
||||
color: white.main,
|
||||
color: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
},
|
||||
|
||||
"&.Mui-completed": {
|
||||
color: white.main,
|
||||
color: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -13,14 +13,8 @@ Coded by www.creative-tim.com
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
import boxShadow from "qqq/assets/theme/functions/boxShadow";
|
||||
|
||||
const { white } = colors;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
@@ -28,9 +22,9 @@ type Types = any;
|
||||
const stepIcon: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
background: "#9fc9ff",
|
||||
fill: "#9fc9ff",
|
||||
stroke: "#9fc9ff",
|
||||
background: "var(--qqq-stepper-inactive-color, rgba(255, 255, 255, 0.5))",
|
||||
fill: "var(--qqq-stepper-inactive-color, rgba(255, 255, 255, 0.5))",
|
||||
stroke: "var(--qqq-stepper-inactive-color, rgba(255, 255, 255, 0.5))",
|
||||
strokeWidth: pxToRem(10),
|
||||
width: pxToRem(13),
|
||||
height: pxToRem(13),
|
||||
@@ -39,19 +33,19 @@ const stepIcon: Types = {
|
||||
transition: "all 200ms linear",
|
||||
|
||||
"&.Mui-active": {
|
||||
background: white.main,
|
||||
fill: white.main,
|
||||
stroke: white.main,
|
||||
borderColor: white.main,
|
||||
boxShadow: boxShadow([0, 0], [0, 2], white.main, 1),
|
||||
background: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
fill: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
stroke: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
borderColor: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
boxShadow: "0rem 0rem 0rem 0.125rem var(--qqq-stepper-active-color, #ffffff)",
|
||||
},
|
||||
|
||||
"&.Mui-completed": {
|
||||
background: white.main,
|
||||
fill: white.main,
|
||||
stroke: white.main,
|
||||
borderColor: white.main,
|
||||
boxShadow: boxShadow([0, 0], [0, 2], white.main, 1),
|
||||
background: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
fill: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
stroke: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
borderColor: "var(--qqq-stepper-active-color, #ffffff)",
|
||||
boxShadow: "0rem 0rem 0rem 0.125rem var(--qqq-stepper-active-color, #ffffff)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,14 +15,11 @@ Coded by www.creative-tim.com
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import typography from "qqq/assets/theme/base/typography";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
import rgba from "qqq/assets/theme/functions/rgba";
|
||||
|
||||
const { size, fontWeightRegular } = typography;
|
||||
const { white } = colors;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
@@ -33,17 +30,17 @@ const stepLabel: Types = {
|
||||
marginTop: `${pxToRem(8)} !important`,
|
||||
fontWeight: fontWeightRegular,
|
||||
fontSize: size.xs,
|
||||
color: "#9fc9ff",
|
||||
color: "var(--qqq-stepper-inactive-color, rgba(255, 255, 255, 0.5))",
|
||||
textTransform: "uppercase",
|
||||
|
||||
"&.Mui-active": {
|
||||
fontWeight: `${fontWeightRegular} !important`,
|
||||
color: `${rgba(white.main, 0.8)} !important`,
|
||||
color: "var(--qqq-stepper-active-color, rgba(255, 255, 255, 0.9)) !important",
|
||||
},
|
||||
|
||||
"&.Mui-completed": {
|
||||
fontWeight: `${fontWeightRegular} !important`,
|
||||
color: `${rgba(white.main, 0.8)} !important`,
|
||||
color: "var(--qqq-stepper-active-color, rgba(255, 255, 255, 0.9)) !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Elemen
|
||||
return (
|
||||
<Box display="inline-block" ml={standardML} mr={0} width={standardWidth}>
|
||||
<Link to={`${tablePath}/create`}>
|
||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||
<MDButton qqqId="create-new" variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||
Create New
|
||||
</MDButton>
|
||||
</Link>
|
||||
@@ -67,7 +67,7 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
|
||||
{
|
||||
return (
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||
<MDButton qqqId="save" type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
</Box>
|
||||
@@ -88,7 +88,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
|
||||
{
|
||||
return (
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
||||
<MDButton qqqId="delete" variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
||||
Delete
|
||||
</MDButton>
|
||||
</Box>
|
||||
@@ -100,7 +100,7 @@ export function QEditButton(): JSX.Element
|
||||
return (
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<Link to="edit">
|
||||
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
||||
<MDButton qqqId="edit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
||||
Edit
|
||||
</MDButton>
|
||||
</Link>
|
||||
@@ -119,6 +119,7 @@ export function QActionsMenuButton({isOpen, onClickHandler}: QActionsMenuButtonP
|
||||
return (
|
||||
<Box width={standardWidth} ml={1}>
|
||||
<MDButton
|
||||
qqqId="actions-menu"
|
||||
variant={isOpen ? "contained" : "outlined"}
|
||||
color="dark"
|
||||
onClick={onClickHandler}
|
||||
@@ -146,7 +147,7 @@ export function QCancelButton({
|
||||
{
|
||||
return (
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
||||
<MDButton qqqId="cancel" type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
</Box>
|
||||
@@ -169,7 +170,7 @@ export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps):
|
||||
{
|
||||
return (
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||
<MDButton qqqId="submit" type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
</Box>
|
||||
@@ -193,7 +194,7 @@ export function QAlternateButton({label, iconName, disabled, onClick}: QAlternat
|
||||
{
|
||||
return (
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<MDButton type="button" variant="gradient" color="secondary" size="small" fullWidth startIcon={iconName && <Icon>{iconName}</Icon>} onClick={onClick} disabled={disabled}>
|
||||
<MDButton qqqId="alternate" type="button" variant="gradient" color="secondary" size="small" fullWidth startIcon={iconName && <Icon>{iconName}</Icon>} onClick={onClick} disabled={disabled}>
|
||||
{label}
|
||||
</MDButton>
|
||||
</Box>
|
||||
|
||||
@@ -24,6 +24,7 @@ import Button from "@mui/material/Button/Button";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {generateButtonId, generateMenuItemId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
|
||||
interface Props
|
||||
@@ -95,7 +96,7 @@ function MenuButton({label, iconName, options, disabled, callback}: Props)
|
||||
const menuItems: JSX.Element[] = []
|
||||
options.map((option, index) =>
|
||||
{
|
||||
menuItems.push(<MenuItem key={index} onClick={e => menuItemClicked(e, index)}>
|
||||
menuItems.push(<MenuItem key={index} data-qqq-id={generateMenuItemId(undefined, option)} onClick={e => menuItemClicked(e, index)}>
|
||||
{option}
|
||||
</MenuItem>);
|
||||
})
|
||||
@@ -105,6 +106,7 @@ function MenuButton({label, iconName, options, disabled, callback}: Props)
|
||||
<Button
|
||||
ref={anchorRef}
|
||||
id="composition-button"
|
||||
data-qqq-id={generateButtonId(undefined, label)}
|
||||
aria-controls={open ? "composition-menu" : undefined}
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
aria-haspopup="true"
|
||||
|
||||
@@ -35,6 +35,7 @@ import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import {flushSync} from "react-dom";
|
||||
import {generateInputId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
// Declaring props types for FormField
|
||||
interface Props
|
||||
@@ -281,9 +282,10 @@ function QDynamicFormField({
|
||||
}
|
||||
else
|
||||
{
|
||||
const dataQqqId = generateInputId(undefined, name, label);
|
||||
field = (
|
||||
<>
|
||||
<Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||
<Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled} data-qqq-id={dataQqqId}
|
||||
onKeyPress={(e: any) =>
|
||||
{
|
||||
if (e.key === "Enter")
|
||||
|
||||
@@ -33,6 +33,7 @@ import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {generateSelectId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@@ -428,11 +429,13 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
|
||||
autocompleteSX = getAutocompleteOutlinedStyle(isDisabled);
|
||||
}
|
||||
|
||||
const dataQqqId = generateSelectId(undefined, name ?? fieldName, fieldLabel);
|
||||
const autocomplete = (
|
||||
<Box>
|
||||
<Autocomplete
|
||||
id={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"}
|
||||
name={name}
|
||||
data-qqq-id={dataQqqId}
|
||||
sx={autocompleteSX}
|
||||
open={open}
|
||||
fullWidth
|
||||
|
||||
@@ -52,6 +52,7 @@ import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc
|
||||
import RowBuilderWidget from "qqq/components/widgets/misc/RowBuilderWidget";
|
||||
import {FieldRule, FieldRuleAction, FieldRuleTrigger} from "qqq/models/fields/FieldRules";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
@@ -458,8 +459,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
return form
|
||||
}
|
||||
|
||||
return <Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}} elevation={cardElevation}>
|
||||
<MDTypography variant="h6" p={3} pb={1}>
|
||||
return <Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}} elevation={cardElevation} data-qqq-id={`form-section-${sanitizeId(section.name)}`}>
|
||||
<MDTypography variant="h6" p={3} pb={1} data-qqq-id={`form-section-header-${sanitizeId(section.name)}`}>
|
||||
{section.label}
|
||||
</MDTypography>
|
||||
{getSectionHelp(section)}
|
||||
@@ -1557,6 +1558,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
|
||||
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
|
||||
const tableNameForId = tableMetaData ? sanitizeId(tableMetaData.name) : "";
|
||||
const formMode = props.isCopy ? "copy" : (props.id != null ? "edit" : "create");
|
||||
|
||||
let body;
|
||||
|
||||
@@ -1614,7 +1617,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
else
|
||||
{
|
||||
body = (
|
||||
<Box mb={3} className="entityForm">
|
||||
<Box mb={3} className="entityForm" data-qqq-id={`record-${formMode}-${tableNameForId}`}>
|
||||
{
|
||||
(alertContent || warningContent) &&
|
||||
<Grid container spacing={3}>
|
||||
@@ -1720,9 +1723,9 @@ function EntityForm(props: Props): JSX.Element
|
||||
<ScrollToFirstError />
|
||||
|
||||
<Box pb={3} pt={0}>
|
||||
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}>
|
||||
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation} data-qqq-id={`record-${formMode}-header-${tableNameForId}`}>
|
||||
<Box display="flex" p={3} pb={1}>
|
||||
<Box mr={1.5}>
|
||||
<Box mr={1.5} data-qqq-id={`record-${formMode}-avatar-${tableNameForId}`}>
|
||||
<Avatar sx={{bgcolor: accentColor}}>
|
||||
<Icon>
|
||||
{tableMetaData?.iconName}
|
||||
@@ -1730,7 +1733,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
<MDTypography variant="h5">{formTitle}</MDTypography>
|
||||
<MDTypography variant="h5" data-qqq-id={`record-${formMode}-title-${tableNameForId}`}>{formTitle}</MDTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
{t1section && getSectionHelp(t1section)}
|
||||
@@ -1750,7 +1753,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
)) : null}
|
||||
|
||||
{formFieldsBySection &&
|
||||
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"}>
|
||||
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"} data-qqq-id={`record-${formMode}-button-bar-${tableNameForId}`}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
|
||||
<QSaveButton disabled={isSubmitting} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QThemeMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
theme?: QThemeMetaData | null;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** BrandedHeaderBar - renders a branded header bar when enabled via theme.
|
||||
** Uses CSS variables injected from QThemeMetaData for styling.
|
||||
*******************************************************************************/
|
||||
export default function BrandedHeaderBar({theme}: Props): JSX.Element | null
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Check if branded header is enabled via theme prop //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
if (!theme?.brandedHeaderEnabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="header"
|
||||
data-branded-header="true"
|
||||
className="qqq-branded-header-bar"
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 1rem",
|
||||
backgroundColor: "var(--qqq-branded-header-background-color)",
|
||||
color: "var(--qqq-branded-header-text-color)",
|
||||
height: "var(--qqq-branded-header-height, 48px)",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 1200,
|
||||
}}
|
||||
>
|
||||
<Box sx={{display: "flex", alignItems: "center", gap: 2}}>
|
||||
{theme.brandedHeaderLogoPath && (
|
||||
<Box
|
||||
component="img"
|
||||
src={theme.brandedHeaderLogoPath}
|
||||
alt={theme.brandedHeaderLogoAltText || "Logo"}
|
||||
sx={{
|
||||
height: "calc(var(--qqq-branded-header-height, 48px) - 8px)",
|
||||
maxWidth: "200px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
onError={(e: React.SyntheticEvent<HTMLImageElement>) =>
|
||||
{
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{theme.brandedHeaderTagline && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: "var(--qqq-branded-header-text-color)",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{theme.brandedHeaderTagline}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
import {ReactNode, useEffect, useReducer, useState} from "react";
|
||||
import {NavLink, useLocation} from "react-router-dom";
|
||||
|
||||
@@ -331,8 +332,9 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
||||
{...rest}
|
||||
variant="permanent"
|
||||
ownerState={{transparentSidenav, whiteSidenav, miniSidenav, darkMode}}
|
||||
data-qqq-id="sidenav-root"
|
||||
>
|
||||
<Box pt={3} mr={1} pb={0} px={4} textAlign="center">
|
||||
<Box pt={3} mr={1} pb={0} px={4} textAlign="center" data-qqq-id="sidenav-logo-area">
|
||||
<Box
|
||||
display={{xs: "block", xl: "none"}}
|
||||
position="absolute"
|
||||
@@ -370,14 +372,14 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
||||
(darkMode && !transparentSidenav && whiteSidenav)
|
||||
}
|
||||
/>
|
||||
<List>{renderRoutes}</List>
|
||||
<List data-qqq-id="sidenav-menu-list">{renderRoutes}</List>
|
||||
<Divider
|
||||
light={
|
||||
(!darkMode && !whiteSidenav && !transparentSidenav) ||
|
||||
(darkMode && !transparentSidenav && whiteSidenav)
|
||||
}
|
||||
/>
|
||||
<Button onClick={logout}>Log Out</Button>
|
||||
<Button onClick={logout} data-qqq-id="sidenav-logout-button">Log Out</Button>
|
||||
</SidenavRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import ListItemText from "@mui/material/ListItemText";
|
||||
import {ReactNode} from "react";
|
||||
import {collapseArrow, collapseIcon, collapseIconBox, collapseItem, collapseText,} from "qqq/components/horseshoe/sidenav/styles/SideNavCollapse";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
import {generateNavItemId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
// Declaring props types for SideNavCollapse
|
||||
interface Props {
|
||||
@@ -53,11 +54,14 @@ function SideNavCollapse({
|
||||
const [controller] = useMaterialUIController();
|
||||
const {miniSidenav, transparentSidenav, whiteSidenav, darkMode} = controller;
|
||||
|
||||
const dataQqqId = generateNavItemId(undefined, name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem component="li">
|
||||
<ListItem component="li" data-qqq-id={dataQqqId}>
|
||||
<Box
|
||||
{...rest}
|
||||
className={active ? "qqq-sidebar-active" : ""}
|
||||
sx={(theme: any) =>
|
||||
collapseItem(theme, {active, transparentSidenav, whiteSidenav, darkMode})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {Theme} from "@mui/material/styles";
|
||||
import {ReactNode} from "react";
|
||||
import {item, itemArrow, itemContent} from "qqq/components/horseshoe/sidenav/styles/SideNavItem";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
import {generateNavItemId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
// Declaring props types for SideNavCollapse
|
||||
interface Props {
|
||||
@@ -45,11 +46,15 @@ function SideNavItem({color, name, active, nested, children, open, ...rest}: Pro
|
||||
const [controller] = useMaterialUIController();
|
||||
const {miniSidenav, transparentSidenav, whiteSidenav, darkMode} = controller;
|
||||
|
||||
// Generate data-qqq-id for CSS targeting
|
||||
const dataQqqId = generateNavItemId(undefined, name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
{...rest}
|
||||
component="li"
|
||||
data-qqq-id={dataQqqId}
|
||||
sx={(theme) => item(theme, {active, color, transparentSidenav, whiteSidenav, darkMode})}
|
||||
>
|
||||
<Box
|
||||
|
||||
@@ -96,8 +96,9 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
border: "none",
|
||||
margin: "0",
|
||||
borderRadius: "0",
|
||||
height: "100%",
|
||||
top: "unset",
|
||||
// Account for branded header height when present
|
||||
top: "var(--qqq-branded-header-height, 0px)",
|
||||
height: "calc(100vh - var(--qqq-branded-header-height, 0px))",
|
||||
|
||||
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
||||
},
|
||||
|
||||
@@ -21,41 +21,41 @@
|
||||
|
||||
import {Theme} from "@mui/material/styles";
|
||||
|
||||
function collapseItem(theme: Theme, ownerState: any)
|
||||
function collapseItem(theme: Theme, ownerState: any)
|
||||
{
|
||||
const {palette, transitions, breakpoints, boxShadows, borders, functions} = theme;
|
||||
const {active, transparentSidenav, whiteSidenav, darkMode} = ownerState;
|
||||
|
||||
const {white, transparent, dark, grey} = palette;
|
||||
const {white, transparent, grey} = palette;
|
||||
const {md} = boxShadows;
|
||||
const {borderRadius} = borders;
|
||||
const {pxToRem, rgba} = functions;
|
||||
|
||||
return {
|
||||
background: () =>
|
||||
background: () =>
|
||||
{
|
||||
let backgroundValue;
|
||||
|
||||
if (transparentSidenav && darkMode)
|
||||
if (transparentSidenav && darkMode)
|
||||
{
|
||||
backgroundValue = active ? rgba(white.main, 0.2) : transparent.main;
|
||||
}
|
||||
else if (transparentSidenav && !darkMode)
|
||||
else if (transparentSidenav && !darkMode)
|
||||
{
|
||||
backgroundValue = active ? grey[300] : transparent.main;
|
||||
}
|
||||
else if (whiteSidenav)
|
||||
else if (whiteSidenav)
|
||||
{
|
||||
backgroundValue = active ? grey[200] : transparent.main;
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
backgroundValue = active ? rgba(white.main, 0.2) : transparent.main;
|
||||
}
|
||||
|
||||
return backgroundValue;
|
||||
},
|
||||
color: (transparentSidenav && !darkMode) || whiteSidenav ? dark.main : white.main,
|
||||
color: "var(--qqq-sidebar-text-color)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
@@ -79,27 +79,22 @@ function collapseItem(theme: Theme, ownerState: any)
|
||||
},
|
||||
|
||||
"&:hover, &:focus": {
|
||||
backgroundColor:
|
||||
transparentSidenav && !darkMode
|
||||
? grey[300]
|
||||
: rgba(whiteSidenav ? grey[400] : white.main, 0.2),
|
||||
backgroundColor: "var(--qqq-sidebar-hover-background-color)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function collapseIconBox(theme: Theme, ownerState: any)
|
||||
function collapseIconBox(theme: Theme, ownerState: any)
|
||||
{
|
||||
const {palette, transitions, borders, functions} = theme;
|
||||
const {transparentSidenav, whiteSidenav, darkMode} = ownerState;
|
||||
const {transitions, borders, functions} = theme;
|
||||
|
||||
const {white, dark} = palette;
|
||||
const {borderRadius} = borders;
|
||||
const {pxToRem} = functions;
|
||||
|
||||
return {
|
||||
minWidth: pxToRem(32),
|
||||
minHeight: pxToRem(32),
|
||||
color: (transparentSidenav && !darkMode) || whiteSidenav ? dark.main : white.main,
|
||||
color: "var(--qqq-sidebar-icon-color)",
|
||||
borderRadius: borderRadius.md,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
@@ -109,13 +104,13 @@ function collapseIconBox(theme: Theme, ownerState: any)
|
||||
}),
|
||||
|
||||
"& svg, svg g": {
|
||||
color: transparentSidenav || whiteSidenav ? dark.main : white.main,
|
||||
color: "var(--qqq-sidebar-icon-color)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const collapseIcon = ({palette: {white, gradients}}: Theme, {active}: any) => ({
|
||||
color: active ? white.main : gradients.dark.state,
|
||||
const collapseIcon = (_theme: Theme, {active}: any) => ({
|
||||
color: active ? "var(--qqq-sidebar-selected-text-color)" : "var(--qqq-sidebar-icon-color)",
|
||||
});
|
||||
|
||||
function collapseText(theme: any, ownerState: any)
|
||||
@@ -147,40 +142,21 @@ function collapseText(theme: any, ownerState: any)
|
||||
};
|
||||
}
|
||||
|
||||
function collapseArrow(theme: Theme, ownerState: any)
|
||||
function collapseArrow(theme: Theme, ownerState: any)
|
||||
{
|
||||
const {palette, typography, transitions, breakpoints, functions} = theme;
|
||||
const {noCollapse, transparentSidenav, whiteSidenav, miniSidenav, open, active, darkMode} =
|
||||
ownerState;
|
||||
const {typography, transitions, breakpoints, functions} = theme;
|
||||
const {noCollapse, miniSidenav, transparentSidenav, open, active} = ownerState;
|
||||
|
||||
const {white, dark} = palette;
|
||||
const {size} = typography;
|
||||
const {pxToRem, rgba} = functions;
|
||||
const {pxToRem} = functions;
|
||||
|
||||
return {
|
||||
fontSize: `${size.lg} !important`,
|
||||
fontWeight: 700,
|
||||
marginBottom: pxToRem(-1),
|
||||
transform: open ? "rotate(0)" : "rotate(-180deg)",
|
||||
color: () =>
|
||||
{
|
||||
let colorValue;
|
||||
|
||||
if (transparentSidenav && darkMode)
|
||||
{
|
||||
colorValue = open || active ? white.main : rgba(white.main, 0.25);
|
||||
}
|
||||
else if (transparentSidenav || whiteSidenav)
|
||||
{
|
||||
colorValue = open || active ? dark.main : rgba(dark.main, 0.25);
|
||||
}
|
||||
else
|
||||
{
|
||||
colorValue = open || active ? white.main : rgba(white.main, 0.5);
|
||||
}
|
||||
|
||||
return colorValue;
|
||||
},
|
||||
color: open || active ? "var(--qqq-sidebar-selected-text-color)" : "var(--qqq-sidebar-icon-color)",
|
||||
opacity: open || active ? 1 : 0.6,
|
||||
transition: transitions.create(["color", "transform", "opacity"], {
|
||||
easing: transitions.easing.easeInOut,
|
||||
duration: transitions.duration.shorter,
|
||||
|
||||
@@ -23,12 +23,11 @@ import {Theme} from "@mui/material/styles";
|
||||
|
||||
function item(theme: Theme | any, ownerState: any)
|
||||
{
|
||||
const {palette, borders, functions, transitions} = theme;
|
||||
const {active, color, transparentSidenav, whiteSidenav, darkMode} = ownerState;
|
||||
const {palette, borders, transitions} = theme;
|
||||
const {active, color} = ownerState;
|
||||
|
||||
const {transparent, white, grey} = palette;
|
||||
const {transparent} = palette;
|
||||
const {borderRadius} = borders;
|
||||
const {rgba} = functions;
|
||||
|
||||
return {
|
||||
pl: 3,
|
||||
@@ -39,29 +38,16 @@ function item(theme: Theme | any, ownerState: any)
|
||||
cursor: "pointer",
|
||||
backgroundColor: () =>
|
||||
{
|
||||
let backgroundValue = transparent.main;
|
||||
|
||||
if (
|
||||
(active === "isParent" && !transparentSidenav && !whiteSidenav) ||
|
||||
(active === "isParent" && transparentSidenav && darkMode)
|
||||
)
|
||||
if (active === "isParent")
|
||||
{
|
||||
backgroundValue = rgba(white.main, 0.2);
|
||||
}
|
||||
else if (active === "isParent" && transparentSidenav)
|
||||
{
|
||||
backgroundValue = grey[300];
|
||||
}
|
||||
else if (active === "isParent" && whiteSidenav)
|
||||
{
|
||||
backgroundValue = grey[200];
|
||||
return "var(--qqq-sidebar-selected-background-color)";
|
||||
}
|
||||
else if (active)
|
||||
{
|
||||
backgroundValue = palette[color].main;
|
||||
return palette[color].main;
|
||||
}
|
||||
|
||||
return backgroundValue;
|
||||
return transparent.main;
|
||||
},
|
||||
transition: transitions.create("background-color", {
|
||||
easing: transitions.easing.easeInOut,
|
||||
@@ -69,19 +55,16 @@ function item(theme: Theme | any, ownerState: any)
|
||||
}),
|
||||
|
||||
"&:hover, &:focus": {
|
||||
backgroundColor:
|
||||
!active &&
|
||||
rgba((transparentSidenav && !darkMode) || whiteSidenav ? grey[400] : white.main, 0.2),
|
||||
backgroundColor: !active ? "var(--qqq-sidebar-hover-background-color)" : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function itemContent(theme: Theme, ownerState: any)
|
||||
{
|
||||
const {palette, typography, transitions, functions} = theme;
|
||||
const {miniSidenav, name, active, transparentSidenav, whiteSidenav, darkMode} = ownerState;
|
||||
const {typography, transitions, functions} = theme;
|
||||
const {miniSidenav, active} = ownerState;
|
||||
|
||||
const {white, dark} = palette;
|
||||
const {size, fontWeightRegular, fontWeightLight} = typography;
|
||||
const {pxToRem} = functions;
|
||||
|
||||
@@ -96,10 +79,7 @@ function itemContent(theme: Theme, ownerState: any)
|
||||
position: "relative",
|
||||
|
||||
"& span": {
|
||||
color:
|
||||
((transparentSidenav && !darkMode) || whiteSidenav) && (active === "isParent" || !active)
|
||||
? dark.main
|
||||
: white.main,
|
||||
color: active ? "var(--qqq-sidebar-selected-text-color)" : "var(--qqq-sidebar-text-color)",
|
||||
fontWeight: active ? fontWeightRegular : fontWeightLight,
|
||||
fontSize: size.sm,
|
||||
opacity: miniSidenav ? 0 : 1,
|
||||
@@ -110,10 +90,7 @@ function itemContent(theme: Theme, ownerState: any)
|
||||
},
|
||||
|
||||
"&::before": {
|
||||
color:
|
||||
((transparentSidenav && !darkMode) || whiteSidenav) && (active === "isParent" || !active)
|
||||
? dark.main
|
||||
: white.main,
|
||||
color: active ? "var(--qqq-sidebar-selected-text-color)" : "var(--qqq-sidebar-text-color)",
|
||||
fontWeight: fontWeightRegular,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -130,38 +107,19 @@ function itemContent(theme: Theme, ownerState: any)
|
||||
|
||||
function itemArrow(theme: Theme, ownerState: any)
|
||||
{
|
||||
const {palette, typography, transitions, breakpoints, functions} = theme;
|
||||
const {noCollapse, transparentSidenav, whiteSidenav, miniSidenav, open, active, darkMode} =
|
||||
ownerState;
|
||||
const {typography, transitions, breakpoints, functions} = theme;
|
||||
const {noCollapse, transparentSidenav, miniSidenav, open, active} = ownerState;
|
||||
|
||||
const {white, dark} = palette;
|
||||
const {size} = typography;
|
||||
const {pxToRem, rgba} = functions;
|
||||
const {pxToRem} = functions;
|
||||
|
||||
return {
|
||||
fontSize: `${size.lg} !important`,
|
||||
fontWeight: 700,
|
||||
marginBottom: pxToRem(-1),
|
||||
transform: open ? "rotate(0)" : "rotate(-180deg)",
|
||||
color: () =>
|
||||
{
|
||||
let colorValue;
|
||||
|
||||
if (transparentSidenav && darkMode)
|
||||
{
|
||||
colorValue = open || active ? white.main : rgba(white.main, 0.25);
|
||||
}
|
||||
else if (transparentSidenav || whiteSidenav)
|
||||
{
|
||||
colorValue = open || active ? dark.main : rgba(dark.main, 0.25);
|
||||
}
|
||||
else
|
||||
{
|
||||
colorValue = open || active ? white.main : rgba(white.main, 0.5);
|
||||
}
|
||||
|
||||
return colorValue;
|
||||
},
|
||||
color: open || active ? "var(--qqq-sidebar-selected-text-color)" : "var(--qqq-sidebar-icon-color)",
|
||||
opacity: open || active ? 1 : 0.6,
|
||||
transition: transitions.create(["color", "transform", "opacity"], {
|
||||
easing: transitions.easing.easeInOut,
|
||||
duration: transitions.duration.shorter,
|
||||
|
||||
@@ -167,11 +167,24 @@ export default styled(Button)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
// styles for the button with variant="gradient"
|
||||
const gradientStyles = () =>
|
||||
{
|
||||
// background value
|
||||
// Map color names to CSS variable names
|
||||
const colorToCssVar: Record<string, string> = {
|
||||
primary: "var(--qqq-primary-color)",
|
||||
secondary: "var(--qqq-secondary-color)",
|
||||
info: "var(--qqq-info-color)",
|
||||
success: "var(--qqq-success-color)",
|
||||
warning: "var(--qqq-warning-color)",
|
||||
error: "var(--qqq-error-color)",
|
||||
};
|
||||
|
||||
// Use CSS variable if available, otherwise fall back to gradient
|
||||
const cssVarColor = colorToCssVar[color];
|
||||
const backgroundValue =
|
||||
color === "white" || !gradients[color]
|
||||
? white.main
|
||||
: linearGradient(gradients[color].main, gradients[color].state);
|
||||
: cssVarColor
|
||||
? cssVarColor
|
||||
: linearGradient(gradients[color].main, gradients[color].state);
|
||||
|
||||
// boxShadow value
|
||||
const boxShadowValue = colored[color]
|
||||
|
||||
@@ -23,6 +23,7 @@ import {ButtonProps} from "@mui/material";
|
||||
import {FC, forwardRef, ReactNode} from "react";
|
||||
import MDButtonRoot from "qqq/components/legacy/MDButton/MDButtonRoot";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
import {generateButtonId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
// Declaring props types for MDButton
|
||||
export interface Props extends Omit<ButtonProps, "color" | "variant"> {
|
||||
@@ -42,19 +43,35 @@ export interface Props extends Omit<ButtonProps, "color" | "variant"> {
|
||||
circular?: boolean;
|
||||
iconOnly?: boolean;
|
||||
children?: ReactNode;
|
||||
qqqId?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const MDButton: FC<Props> = forwardRef(
|
||||
({color, variant, size, circular, iconOnly, children, ...rest}, ref) =>
|
||||
({color, variant, size, circular, iconOnly, children, qqqId, ...rest}, ref) =>
|
||||
{
|
||||
const [controller] = useMaterialUIController();
|
||||
const {darkMode} = controller;
|
||||
|
||||
// Extract icon name from startIcon if present (for icon-only buttons)
|
||||
let iconName: string | undefined;
|
||||
if (rest.startIcon && typeof rest.startIcon === "object")
|
||||
{
|
||||
const iconProps = (rest.startIcon as any).props;
|
||||
if (iconProps?.children)
|
||||
{
|
||||
iconName = String(iconProps.children);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate data-qqq-id for CSS targeting
|
||||
const dataQqqId = generateButtonId(qqqId, children, iconName);
|
||||
|
||||
return (
|
||||
<MDButtonRoot
|
||||
{...rest}
|
||||
ref={ref}
|
||||
data-qqq-id={dataQqqId}
|
||||
ownerState={{color, variant, size, circular, iconOnly, darkMode}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {Theme} from "@mui/material/styles";
|
||||
import React from "react";
|
||||
import {HashLink} from "react-router-hash-link";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@@ -82,7 +83,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, sectionVisibility, s
|
||||
|
||||
|
||||
return (
|
||||
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "hidden", maxHeight: "calc(100vh - 2rem)"}}>
|
||||
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "hidden", maxHeight: "calc(100vh - 2rem)"}} data-qqq-id="record-sidebar">
|
||||
<Box component="ul" display="flex" flexDirection={{xs: "row", md: "column"}} flexWrap={{xs: "wrap", md: "nowrap"}} p={2} m={0} sx={{listStyle: "none", overflow: "auto", height: "100%"}}>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -91,7 +92,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, sectionVisibility, s
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
||||
|
||||
<Box key={`section-link-${entry.name}`} className={`sidebar-section ${sectionVisibility[entry.name] === false ? "is-hidden" : "is-visible"}`} onClick={() => document.getElementById(entry.name)?.scrollIntoView()} sx={{cursor: "pointer"}} width={{xs: "50%", md: "100%"}} >
|
||||
<Box key={`section-link-${entry.name}`} className={`sidebar-section ${sectionVisibility[entry.name] === false ? "is-hidden" : "is-visible"}`} onClick={() => document.getElementById(entry.name)?.scrollIntoView()} sx={{cursor: "pointer"}} width={{xs: "50%", md: "100%"}} data-qqq-id={`sidebar-item-${sanitizeId(entry.name)}`}>
|
||||
<Box key={`section-${entry.name}`} component="li" pt={0.5} pb={0.5}>
|
||||
<MDTypography
|
||||
variant="button"
|
||||
|
||||
@@ -43,6 +43,7 @@ import FormData from "form-data";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {QCancelButton, QDeleteButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import {generateButtonId} from "qqq/utils/qqqIdUtils";
|
||||
import RecordQueryView from "qqq/models/query/RecordQueryView";
|
||||
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
@@ -635,6 +636,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
||||
<Box order="1" mr={"0.5rem"}>
|
||||
<Button
|
||||
onClick={openSavedViewsMenu}
|
||||
data-qqq-id="button-views"
|
||||
sx={{
|
||||
borderRadius: "0.75rem",
|
||||
textTransform: "none",
|
||||
|
||||
@@ -24,8 +24,8 @@ import Card from "@mui/material/Card";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@@ -39,16 +39,20 @@ interface Props
|
||||
};
|
||||
icon: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
qqqId?: string;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function ProcessLinkCard({
|
||||
color, isReport, title, percentage, icon, isDisabled
|
||||
color, isReport, title, percentage, icon, isDisabled, qqqId
|
||||
}: Props): JSX.Element
|
||||
{
|
||||
const cardType = isReport ? "report-card" : "process-card";
|
||||
const generatedId = qqqId ? `${cardType}-${sanitizeId(qqqId)}` : `${cardType}-${sanitizeId(title)}`;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card data-qqq-id={generatedId}>
|
||||
<Box display="flex" justifyContent="space-between" pt={3} px={2} title={isDisabled ? `You do not have permission to access this ${isReport ? "report" : "process"}` : ""}>
|
||||
<Box
|
||||
color={color === "light" ? "#000000" : "#FFFFFF"}
|
||||
@@ -59,7 +63,13 @@ function ProcessLinkCard({
|
||||
width="4rem"
|
||||
height="4rem"
|
||||
mt={-1.5}
|
||||
sx={{borderRadius: "10px", backgroundColor: isDisabled ? colors.secondary.main : colors.info.main}}
|
||||
sx={{
|
||||
borderRadius: "10px",
|
||||
backgroundColor: isDisabled
|
||||
? "var(--qqq-secondary-color, #7b809a)"
|
||||
: "var(--qqq-info-color, #0062FF)"
|
||||
}}
|
||||
data-qqq-id={`${generatedId}-icon`}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{icon}
|
||||
@@ -109,6 +119,7 @@ ProcessLinkCard.defaultProps = {
|
||||
label: "",
|
||||
},
|
||||
isDisabled: false,
|
||||
qqqId: undefined,
|
||||
};
|
||||
|
||||
export default ProcessLinkCard;
|
||||
@@ -84,10 +84,10 @@ function ProcessSummaryResults({
|
||||
</List>
|
||||
);
|
||||
|
||||
let headerColor = colors.success.main;
|
||||
let headerColor = "var(--qqq-success-color)";
|
||||
if(processValues.status == "ERROR")
|
||||
{
|
||||
headerColor = colors.error.main;
|
||||
headerColor = "var(--qqq-error-color)";
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -95,7 +95,7 @@ function ProcessSummaryResults({
|
||||
<Grid container>
|
||||
<Grid item xs={0} lg={2} />
|
||||
<Grid item xs={12} lg={8}>
|
||||
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="10px" p={2} mt={2}>
|
||||
<Box border="1px solid var(--qqq-border-color, rgb(70%, 70%, 70%))" borderRadius="10px" p={2} mt={2}>
|
||||
<Box mx={2} mt={-5} p={1} sx={{width: "fit-content", borderRadius: ".25em", backgroundColor: headerColor}} width="initial" color="#FFFFFF">
|
||||
<Box display="flex" alignItems="center" color="white">
|
||||
{process.iconName && <Icon fontSize="medium" sx={{mr: 1}}>{process.iconName}</Icon>}
|
||||
|
||||
@@ -268,8 +268,8 @@ function ValidationReview({
|
||||
|
||||
|
||||
const recordPreviewWidget = step.recordListFields && (
|
||||
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="10px" p={2} mt={2}>
|
||||
<Box mx={2} mt={-5} p={1} sx={{width: "fit-content", borderColor: "rgb(70%, 70%, 70%)", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "#FFFFFF"}} width="initial" color="white">
|
||||
<Box border="1px solid var(--qqq-border-color, rgb(70%, 70%, 70%))" borderRadius="10px" p={2} mt={2}>
|
||||
<Box mx={2} mt={-5} p={1} sx={{width: "fit-content", borderColor: "var(--qqq-border-color, rgb(70%, 70%, 70%))", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "var(--qqq-surface-color, #FFFFFF)"}} width="initial" color="var(--qqq-text-primary)">
|
||||
<Typography>Preview</Typography>
|
||||
</Box>
|
||||
<Box p={3} pb={0}>
|
||||
|
||||
@@ -657,8 +657,8 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
size="small"
|
||||
sx={{pl: 0.5, width: "10rem"}}
|
||||
>
|
||||
<ToggleButton value="basic" disabled={!canFilterWorkAsBasic}>Basic</ToggleButton>
|
||||
<ToggleButton value="advanced">Advanced</ToggleButton>
|
||||
<ToggleButton value="basic" data-qqq-id="button-query-mode-basic" disabled={!canFilterWorkAsBasic}>Basic</ToggleButton>
|
||||
<ToggleButton value="advanced" data-qqq-id="button-query-mode-advanced">Advanced</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -747,6 +747,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
<>
|
||||
<Button
|
||||
className="filterBuilderButton"
|
||||
data-qqq-id="button-filter-builder"
|
||||
onClick={(e) => openFilterBuilder(e)}
|
||||
{...filterBuilderMouseEvents}
|
||||
startIcon={<Icon>build</Icon>}
|
||||
|
||||
@@ -32,6 +32,7 @@ import ListItem, {ListItemProps} from "@mui/material/ListItem/ListItem";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
import React, {useMemo, useState} from "react";
|
||||
|
||||
interface FieldListMenuProps
|
||||
@@ -584,7 +585,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={openMenu} {...buttonProps}>
|
||||
<Button onClick={openMenu} data-qqq-id={`button-${sanitizeId(idPrefix)}`} {...buttonProps}>
|
||||
{buttonChildren}
|
||||
</Button>
|
||||
<Menu
|
||||
|
||||
@@ -32,6 +32,7 @@ import MenuItem from "@mui/material/MenuItem";
|
||||
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {generateMenuItemId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
interface QueryScreenActionMenuProps
|
||||
{
|
||||
@@ -78,19 +79,19 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && metaData.processes.has(`${tableMetaData.name}.bulkInsert`))
|
||||
{
|
||||
menuItems.push(<MenuItem key="bulkLoad" onClick={() => runSomething(bulkLoadClicked)}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>);
|
||||
menuItems.push(<MenuItem key="bulkLoad" data-qqq-id={generateMenuItemId(undefined, "Bulk Load")} onClick={() => runSomething(bulkLoadClicked)}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>);
|
||||
}
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission && metaData.processes.has(`${tableMetaData.name}.bulkEdit`))
|
||||
{
|
||||
menuItems.push(<MenuItem key="bulkEdit" onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
||||
menuItems.push(<MenuItem key="bulkEdit" data-qqq-id={generateMenuItemId(undefined, "Bulk Edit")} onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
||||
}
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission && metaData.processes.has(`${tableMetaData.name}.bulkEditWithFile`))
|
||||
{
|
||||
menuItems.push(<MenuItem key="bulkEditWithFile" onClick={() => runSomething(bulkEditWithFileClicked)}><ListItemIcon><Icon>edit_note</Icon></ListItemIcon>Bulk Edit With File</MenuItem>);
|
||||
menuItems.push(<MenuItem key="bulkEditWithFile" data-qqq-id={generateMenuItemId(undefined, "Bulk Edit With File")} onClick={() => runSomething(bulkEditWithFileClicked)}><ListItemIcon><Icon>edit_note</Icon></ListItemIcon>Bulk Edit With File</MenuItem>);
|
||||
}
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission && metaData.processes.has(`${tableMetaData.name}.bulkDelete`))
|
||||
{
|
||||
menuItems.push(<MenuItem key="bulkDelete" onClick={() => runSomething(bulkDeleteClicked)}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>);
|
||||
menuItems.push(<MenuItem key="bulkDelete" data-qqq-id={generateMenuItemId(undefined, "Bulk Delete")} onClick={() => runSomething(bulkDeleteClicked)}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>);
|
||||
}
|
||||
|
||||
menuItems.push(<Divider key="divider1" />);
|
||||
@@ -98,7 +99,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
tableProcesses.sort((a, b) => a.label.localeCompare(b.label));
|
||||
tableProcesses.map((process) =>
|
||||
{
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
menuItems.push(<MenuItem key={process.name} data-qqq-id={generateMenuItemId(undefined, process.label)} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
});
|
||||
|
||||
menuItems.push(<Divider key="divider2" />);
|
||||
@@ -117,7 +118,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
const process = metaData?.processes.get(processName);
|
||||
if (process)
|
||||
{
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
menuItems.push(<MenuItem key={process.name} data-qqq-id={generateMenuItemId(undefined, process.label)} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,18 +132,18 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
if (runRecordScriptProcess)
|
||||
{
|
||||
const process = runRecordScriptProcess;
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
menuItems.push(<MenuItem key={process.name} data-qqq-id={generateMenuItemId(undefined, process.label)} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// todo - any conditions around this? //
|
||||
////////////////////////////////////////
|
||||
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableMetaData.name)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
||||
menuItems.push(<MenuItem key="developerMode" data-qqq-id={generateMenuItemId(undefined, "Developer Mode")} onClick={() => navigate(`${metaData.getTablePathByName(tableMetaData.name)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
||||
|
||||
if (menuItems.length === 0)
|
||||
{
|
||||
menuItems.push(<MenuItem key="notAvaialableNow" disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>);
|
||||
menuItems.push(<MenuItem key="notAvaialableNow" data-qqq-id={generateMenuItemId(undefined, "No Actions Available")} disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -24,9 +24,9 @@ import Card from "@mui/material/Card";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
|
||||
|
||||
// Decalaring props types for MiniStatisticsCard
|
||||
@@ -47,6 +47,7 @@ interface Props {
|
||||
};
|
||||
direction?: "right" | "left";
|
||||
isDisabled?: boolean;
|
||||
qqqId?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -58,13 +59,16 @@ function MiniStatisticsCard({
|
||||
icon,
|
||||
direction,
|
||||
isDisabled,
|
||||
qqqId,
|
||||
}: Props): JSX.Element
|
||||
{
|
||||
const [controller] = useMaterialUIController();
|
||||
const {darkMode} = controller;
|
||||
|
||||
const generatedId = qqqId ? `table-card-${sanitizeId(qqqId)}` : (title?.text ? `table-card-${sanitizeId(title.text)}` : undefined);
|
||||
|
||||
return (
|
||||
<Card sx={{overflow: "hidden"}}>
|
||||
<Card sx={{overflow: "hidden"}} data-qqq-id={generatedId}>
|
||||
<Box
|
||||
sx={({palette: {background}}: { palette: any }) => ({
|
||||
background: darkMode && background.card,
|
||||
@@ -110,7 +114,13 @@ function MiniStatisticsCard({
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
color="#FFFFFF"
|
||||
sx={{borderRadius: "10px", backgroundColor: isDisabled ? colors.secondary.main : colors.info.main}}
|
||||
sx={{
|
||||
borderRadius: "10px",
|
||||
backgroundColor: isDisabled
|
||||
? "var(--qqq-secondary-color, #7b809a)"
|
||||
: "var(--qqq-info-color, #0062FF)"
|
||||
}}
|
||||
data-qqq-id={generatedId ? `${generatedId}-icon` : undefined}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{icon.component}
|
||||
@@ -137,6 +147,7 @@ MiniStatisticsCard.defaultProps = {
|
||||
},
|
||||
direction: "right",
|
||||
isDisabled: false,
|
||||
qqqId: undefined,
|
||||
};
|
||||
|
||||
export default MiniStatisticsCard;
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createContext, FC, ReactNode, useContext, useMemo} from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Context value interface for QQQ ID scoping.
|
||||
*******************************************************************************/
|
||||
interface QqqIdContextValue
|
||||
{
|
||||
scope: string | null;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Create the context with default null scope.
|
||||
*******************************************************************************/
|
||||
const QqqIdContext = createContext<QqqIdContextValue>({scope: null});
|
||||
QqqIdContext.displayName = "QqqIdContext";
|
||||
|
||||
/*******************************************************************************
|
||||
** Props for the QqqIdProvider component.
|
||||
*******************************************************************************/
|
||||
interface QqqIdProviderProps
|
||||
{
|
||||
scope: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Provider component that sets the data-qqq-scope attribute on its children.
|
||||
** Use this to wrap pages or sections to provide context for CSS selectors.
|
||||
**
|
||||
** Example:
|
||||
** <QqqIdProvider scope="record-view">
|
||||
** <RecordViewContent />
|
||||
** </QqqIdProvider>
|
||||
**
|
||||
** CSS can then target:
|
||||
** [data-qqq-scope="record-view"] [data-qqq-id="button-edit"] { ... }
|
||||
*******************************************************************************/
|
||||
export const QqqIdProvider: FC<QqqIdProviderProps> = ({scope, children}) =>
|
||||
{
|
||||
const value = useMemo(() => ({scope}), [scope]);
|
||||
|
||||
return (
|
||||
<QqqIdContext.Provider value={value}>
|
||||
<div data-qqq-scope={scope}>
|
||||
{children}
|
||||
</div>
|
||||
</QqqIdContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Hook to access the current QQQ ID scope.
|
||||
** Returns null if not within a QqqIdProvider.
|
||||
*******************************************************************************/
|
||||
export function useQqqScope(): string | null
|
||||
{
|
||||
const context = useContext(QqqIdContext);
|
||||
return context.scope;
|
||||
}
|
||||
|
||||
export default QqqIdContext;
|
||||
+16
-15
@@ -32,13 +32,13 @@ import Card from "@mui/material/Card";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import ProcessLinkCard from "qqq/components/processes/ProcessLinkCard";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
import MiniStatisticsCard from "qqq/components/widgets/statistics/MiniStatisticsCard";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {Link, useLocation} from "react-router-dom";
|
||||
|
||||
@@ -233,21 +233,21 @@ function AppHome({app}: Props): JSX.Element
|
||||
<BaseLayout>
|
||||
{
|
||||
showAppLabelOnHomeScreen &&
|
||||
<Typography textTransform="capitalize" variant="h3">
|
||||
<Typography textTransform="capitalize" variant="h3" data-qqq-id={`app-header-${sanitizeId(app.name)}`}>
|
||||
{app.label}
|
||||
</Typography>
|
||||
}
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={12}>
|
||||
<Card sx={{overflow: "visible"}}>
|
||||
<Card sx={{overflow: "visible"}} data-qqq-id="app-section-child-apps">
|
||||
<Box p={3} display="flex" alignItems="center" gap=".5rem">
|
||||
<Typography variant="h5">Apps</Typography>
|
||||
<Typography variant="h5" data-qqq-id="section-header-apps">Apps</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={3} padding={3} pt={0}>
|
||||
{childApps.map((childApp) => (
|
||||
<Grid key={childApp.name} item xs={12} lg={3}>
|
||||
<Link to={childApp.name}>
|
||||
<Card>
|
||||
<Card data-qqq-id={`app-card-${sanitizeId(childApp.name)}`}>
|
||||
<Box display="flex" alignItems="center" p={2}>
|
||||
<Box
|
||||
color={"#FFFFFF"}
|
||||
@@ -256,7 +256,8 @@ function AppHome({app}: Props): JSX.Element
|
||||
alignItems="center"
|
||||
width="4rem"
|
||||
height="4rem"
|
||||
sx={{borderRadius: "10px", backgroundColor: colors.info.main}}
|
||||
sx={{borderRadius: "10px", backgroundColor: "var(--qqq-info-color, #0062FF)"}}
|
||||
data-qqq-id={`app-card-${sanitizeId(childApp.name)}-icon`}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{childApp.iconName || app.iconName}
|
||||
@@ -284,13 +285,13 @@ function AppHome({app}: Props): JSX.Element
|
||||
<BaseLayout>
|
||||
{
|
||||
showAppLabelOnHomeScreen &&
|
||||
<Typography textTransform="capitalize" variant="h3">
|
||||
<Typography textTransform="capitalize" variant="h3" data-qqq-id={`app-header-${sanitizeId(app.name)}`}>
|
||||
{app.label}
|
||||
</Typography>
|
||||
}
|
||||
<Box>
|
||||
<Box data-qqq-id={`app-home-${sanitizeId(app.name)}`}>
|
||||
{app.widgets && app.widgets.length > 0 && (
|
||||
<Box pb={app.sections ? 2.375 : 4} pt={"0.5rem"}>
|
||||
<Box pb={app.sections ? 2.375 : 4} pt={"0.5rem"} data-qqq-id="app-widgets-container">
|
||||
<DashboardWidgets widgetMetaDataList={widgets} />
|
||||
</Box>
|
||||
)}
|
||||
@@ -300,22 +301,22 @@ function AppHome({app}: Props): JSX.Element
|
||||
<Grid item xs={12} lg={12}>
|
||||
{app.sections.map((section) => (
|
||||
<Box key={section.name} mb={3}>
|
||||
<Card sx={{overflow: "visible"}}>
|
||||
<Card sx={{overflow: "visible"}} data-qqq-id={`app-section-${sanitizeId(section.name)}`}>
|
||||
<Box p={3} display="flex" alignItems="center" gap=".5rem">
|
||||
{
|
||||
section.icon &&
|
||||
(
|
||||
section.icon.path && <img src={section.icon.path} alt={section.label} />
|
||||
section.icon.path && <img src={section.icon.path} alt={section.label} data-qqq-id={`section-icon-${sanitizeId(section.name)}`} />
|
||||
)
|
||||
}
|
||||
<Typography variant="h5">
|
||||
<Typography variant="h5" data-qqq-id={`section-header-${sanitizeId(section.name)}`}>
|
||||
{section.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
{
|
||||
section.processes ? (
|
||||
<Box p={3} pl={3} pt={0} pb={1}>
|
||||
<MDTypography variant="h6">Actions</MDTypography>
|
||||
<MDTypography variant="h6" data-qqq-id={`section-subheader-${sanitizeId(section.name)}-actions`}>Actions</MDTypography>
|
||||
</Box>
|
||||
) : null
|
||||
}
|
||||
@@ -356,7 +357,7 @@ function AppHome({app}: Props): JSX.Element
|
||||
{
|
||||
section.reports ? (
|
||||
<Box p={3} pl={3} pt={0} pb={1}>
|
||||
<MDTypography variant="h6">Reports</MDTypography>
|
||||
<MDTypography variant="h6" data-qqq-id={`section-subheader-${sanitizeId(section.name)}-reports`}>Reports</MDTypography>
|
||||
</Box>
|
||||
) : null
|
||||
}
|
||||
@@ -399,7 +400,7 @@ function AppHome({app}: Props): JSX.Element
|
||||
{
|
||||
section.tables ? (
|
||||
<Box p={3} pl={3} pb={1} pt={0}>
|
||||
<MDTypography variant="h6">Data</MDTypography>
|
||||
<MDTypography variant="h6" data-qqq-id={`section-subheader-${sanitizeId(section.name)}-data`}>Data</MDTypography>
|
||||
</Box>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -780,7 +780,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
{
|
||||
showHelp &&
|
||||
<Box fontSize={"0.875rem"} color={colors.blueGray.main} pb={2}>
|
||||
<Box fontSize={"0.875rem"} color="var(--qqq-text-secondary)" pb={2}>
|
||||
{formattedHelpContent}
|
||||
</Box>
|
||||
}
|
||||
@@ -962,8 +962,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
component.type === QComponentType.DOWNLOAD_FORM && (
|
||||
<Grid container display="flex" justifyContent="center">
|
||||
<Grid item xs={12} sm={12} xl={8} m={3} p={3} mt={6} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
|
||||
<Box mx={2} mt={-6} p={1} sx={{width: "fit-content", borderColor: "rgb(70%, 70%, 70%)", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "#FFFFFF"}} width="initial" color="white">
|
||||
<Grid item xs={12} sm={12} xl={8} m={3} p={3} mt={6} sx={{border: "1px solid var(--qqq-border-color, gray)", borderRadius: "1rem"}}>
|
||||
<Box mx={2} mt={-6} p={1} sx={{width: "fit-content", borderColor: "var(--qqq-border-color, rgb(70%, 70%, 70%))", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "var(--qqq-surface-color, #FFFFFF)"}} width="initial" color="var(--qqq-text-primary)">
|
||||
Download
|
||||
</Box>
|
||||
<Box display="flex" py={1} pr={2}>
|
||||
@@ -2154,7 +2154,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
||||
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
|
||||
{
|
||||
mainCardStyles.background = "#FFFFFF";
|
||||
mainCardStyles.background = "var(--qqq-surface-color, #FFFFFF)";
|
||||
mainCardStyles.boxShadow = "none";
|
||||
}
|
||||
|
||||
|
||||
@@ -3311,7 +3311,7 @@ const RecordQueryInner = forwardRef(({table, apiVersion, usage, isModal, isPrevi
|
||||
getRowId={(row) => row.__rowIndex}
|
||||
selectionModel={rowSelectionModel}
|
||||
hideFooterSelectedRowCount={true}
|
||||
sx={{border: 0, height: `calc(100vh - ${spaceAboveGrid + spaceBelowGrid}px)`}}
|
||||
sx={{border: 0, height: `calc(100vh - ${spaceAboveGrid + spaceBelowGrid}px - var(--qqq-branded-header-height, 0px))`}}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
@@ -50,6 +50,7 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import {SxProps} from "@mui/system";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {sanitizeId} from "qqq/utils/qqqIdUtils";
|
||||
import AuditBody from "qqq/components/audits/AuditBody";
|
||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton, standardWidth} from "qqq/components/buttons/DefaultButtons";
|
||||
import EntityForm from "qqq/components/forms/EntityForm";
|
||||
@@ -642,8 +643,8 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
sectionFieldElements.set(section.name,
|
||||
<Grid id={section.name} key={section.name} item lg={section.gridColumns ?? 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
<Box width="100%">
|
||||
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px", height: "100%"}}>
|
||||
<Typography variant="h6" p={3} pb={1}>
|
||||
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px", height: "100%"}} data-qqq-id={`record-section-${sanitizeId(section.name)}`}>
|
||||
<Typography variant="h6" p={3} pb={1} data-qqq-id={`section-header-${sanitizeId(section.name)}`}>
|
||||
{section.label}
|
||||
</Typography>
|
||||
{getSectionHelp(section)}
|
||||
@@ -774,35 +775,38 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
open={Boolean(actionsMenu)}
|
||||
onClose={closeActionsMenu}
|
||||
keepMounted
|
||||
data-qqq-id="record-view-actions-menu"
|
||||
>
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
||||
<MenuItem onClick={() => gotoCreate()}>
|
||||
<MenuItem onClick={() => gotoCreate()} data-qqq-id="menu-item-new">
|
||||
<ListItemIcon><Icon>add</Icon></ListItemIcon>
|
||||
New
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
||||
<MenuItem onClick={() => navigate("copy")}>
|
||||
<MenuItem onClick={() => navigate("copy")} data-qqq-id="menu-item-copy">
|
||||
<ListItemIcon><Icon>copy</Icon></ListItemIcon>
|
||||
Copy
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission &&
|
||||
<MenuItem onClick={() => navigate("edit")}>
|
||||
<MenuItem onClick={() => navigate("edit")} data-qqq-id="menu-item-edit">
|
||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||
Edit
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission &&
|
||||
<MenuItem onClick={() =>
|
||||
{
|
||||
setActionsMenu(null);
|
||||
handleClickDeleteButton();
|
||||
}}
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
{
|
||||
setActionsMenu(null);
|
||||
handleClickDeleteButton();
|
||||
}}
|
||||
data-qqq-id="menu-item-delete"
|
||||
>
|
||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||
Delete
|
||||
@@ -810,7 +814,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
}
|
||||
{tableProcesses?.length > 0 && hasEditOrDelete && <Divider />}
|
||||
{tableProcesses?.map((process) => (
|
||||
<MenuItem key={process.name} onClick={() => processClicked(process)}>
|
||||
<MenuItem key={process.name} onClick={() => processClicked(process)} data-qqq-id={`menu-item-${sanitizeId(process.name)}`}>
|
||||
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
||||
{process.label}
|
||||
</MenuItem>
|
||||
@@ -820,13 +824,13 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
getGenericProcesses(metaData).map((process) =>
|
||||
(
|
||||
process &&
|
||||
<MenuItem key={process.name} onClick={() => processClicked(process)}>
|
||||
<MenuItem key={process.name} onClick={() => processClicked(process)} data-qqq-id={`menu-item-${sanitizeId(process.name)}`}>
|
||||
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
||||
{process.label}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
<MenuItem onClick={() => navigate("dev")}>
|
||||
<MenuItem onClick={() => navigate("dev")} data-qqq-id="menu-item-developer-mode">
|
||||
<ListItemIcon><Icon>code</Icon></ListItemIcon>
|
||||
Developer Mode
|
||||
</MenuItem>
|
||||
@@ -836,7 +840,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
{
|
||||
setActionsMenu(null);
|
||||
navigate("#audit");
|
||||
}}>
|
||||
}} data-qqq-id="menu-item-audit">
|
||||
<ListItemIcon><Icon>checklist</Icon></ListItemIcon>
|
||||
Audit
|
||||
</MenuItem>
|
||||
@@ -995,9 +999,11 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
}
|
||||
};
|
||||
|
||||
const tableNameForId = tableMetaData ? sanitizeId(tableMetaData.name) : "";
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Box className="recordView">
|
||||
<Box className="recordView" data-qqq-id={`record-view-${tableNameForId}`}>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Box mb={3}>
|
||||
@@ -1046,9 +1052,9 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} mb={3}>
|
||||
<Card id={t1SectionName} sx={{scrollMarginTop: "100px", minHeight: "88px"}}>
|
||||
<Card id={t1SectionName} sx={{scrollMarginTop: "100px", minHeight: "88px"}} data-qqq-id={`record-view-header-${tableNameForId}`}>
|
||||
<Box display="flex" p={3} pb={1}>
|
||||
<Box mr={1.5}>
|
||||
<Box mr={1.5} data-qqq-id={`record-view-avatar-${tableNameForId}`}>
|
||||
<Avatar sx={{bgcolor: accentColor}}>
|
||||
<Icon>
|
||||
{tableMetaData?.iconName}
|
||||
@@ -1056,7 +1062,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" width="100%" alignItems="flex-start" flexWrap={{xs: "wrap", md: "nowrap"}}>
|
||||
<Typography variant="h5" mb="0.5rem">
|
||||
<Typography variant="h5" mb="0.5rem" data-qqq-id={`record-view-title-${tableNameForId}`}>
|
||||
{tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel || ""}` : ""}
|
||||
</Typography>
|
||||
<Box display="flex" ml="auto">
|
||||
@@ -1083,7 +1089,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
</Grid>
|
||||
{
|
||||
tableMetaData && record && ((table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission) || (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)) &&
|
||||
<Box component="div" p={3} className={"stickyBottomButtonBar"}>
|
||||
<Box component="div" p={3} className={"stickyBottomButtonBar"} data-qqq-id={`record-view-button-bar-${tableNameForId}`}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && <QDeleteButton onClickHandler={handleClickDeleteButton} />
|
||||
@@ -1104,16 +1110,17 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
onClose={handleDeleteConfirmClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
data-qqq-id="delete-confirmation-dialog"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Confirm Deletion</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title" data-qqq-id="delete-confirmation-title">Confirm Deletion</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
<DialogContentText id="alert-dialog-description" data-qqq-id="delete-confirmation-text">
|
||||
Are you sure you want to delete this record?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteConfirmClose}>No</Button>
|
||||
<Button onClick={handleDelete} autoFocus disabled={isDeleteSubmitting}>
|
||||
<DialogActions data-qqq-id="delete-confirmation-actions">
|
||||
<Button onClick={handleDeleteConfirmClose} data-qqq-id="button-delete-no">No</Button>
|
||||
<Button onClick={handleDelete} autoFocus disabled={isDeleteSubmitting} data-qqq-id="button-delete-yes">
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -893,3 +893,213 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
{
|
||||
background-position: right 1.75rem center;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Base typography theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
body,
|
||||
html,
|
||||
.MuiTypography-root
|
||||
{
|
||||
font-size: var(--qqq-font-size-base, 14px) !important;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Sidebar theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
.MuiDrawer-paper
|
||||
{
|
||||
background: var(--qqq-sidebar-background-color) !important;
|
||||
}
|
||||
|
||||
.MuiDrawer-paper a,
|
||||
.MuiDrawer-paper .MuiTypography-root,
|
||||
.MuiDrawer-paper .MuiListItemText-primary,
|
||||
.MuiDrawer-paper .MuiListItemText-root span
|
||||
{
|
||||
color: var(--qqq-sidebar-text-color) !important;
|
||||
}
|
||||
|
||||
.MuiDrawer-paper svg,
|
||||
.MuiDrawer-paper .MuiIcon-root,
|
||||
.MuiDrawer-paper .MuiSvgIcon-root,
|
||||
.MuiDrawer-paper .MuiListItemIcon-root,
|
||||
.MuiDrawer-paper .MuiListItemIcon-root .MuiIcon-root,
|
||||
.MuiDrawer-paper .MuiListItemIcon-root .MuiSvgIcon-root,
|
||||
.MuiDrawer-paper .MuiListItemIcon-root svg
|
||||
{
|
||||
color: var(--qqq-sidebar-icon-color) !important;
|
||||
}
|
||||
|
||||
.MuiDrawer-paper .MuiDivider-root
|
||||
{
|
||||
border-color: var(--qqq-sidebar-divider-color);
|
||||
}
|
||||
|
||||
.MuiDrawer-paper .qqq-sidebar-active
|
||||
{
|
||||
background-color: var(--qqq-sidebar-selected-background-color) !important;
|
||||
}
|
||||
|
||||
.MuiDrawer-paper .qqq-sidebar-active,
|
||||
.MuiDrawer-paper .qqq-sidebar-active span,
|
||||
.MuiDrawer-paper .qqq-sidebar-active .MuiListItemText-primary,
|
||||
.MuiDrawer-paper .qqq-sidebar-active .MuiIcon-root,
|
||||
.MuiDrawer-paper .qqq-sidebar-active .MuiSvgIcon-root,
|
||||
.MuiDrawer-paper .qqq-sidebar-active svg,
|
||||
.MuiDrawer-paper .qqq-sidebar-active .MuiListItemIcon-root,
|
||||
.MuiDrawer-paper .qqq-sidebar-active .MuiListItemIcon-root .MuiIcon-root,
|
||||
.MuiDrawer-paper .qqq-sidebar-active .MuiListItemIcon-root svg
|
||||
{
|
||||
color: var(--qqq-sidebar-selected-text-color) !important;
|
||||
}
|
||||
|
||||
.MuiDrawer-paper a:hover,
|
||||
.MuiDrawer-paper .MuiListItem-root:hover
|
||||
{
|
||||
background-color: var(--qqq-sidebar-hover-background-color);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Input field theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
input.MuiInputBase-input,
|
||||
.MuiOutlinedInput-input,
|
||||
.MuiFilledInput-input,
|
||||
.MuiInput-input
|
||||
{
|
||||
color: var(--qqq-text-primary) !important;
|
||||
}
|
||||
|
||||
.MuiOutlinedInput-notchedOutline
|
||||
{
|
||||
border-color: var(--qqq-input-border-color, var(--qqq-border-color)) !important;
|
||||
}
|
||||
|
||||
.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline
|
||||
{
|
||||
border-color: var(--qqq-input-border-color, var(--qqq-border-color)) !important;
|
||||
}
|
||||
|
||||
.Mui-focused .MuiOutlinedInput-notchedOutline
|
||||
{
|
||||
border-color: var(--qqq-primary-color) !important;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Link theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
a:not(.MuiDrawer-root *):not(.qqq-branded-header-bar *):not(.MuiButtonBase-root),
|
||||
.MuiLink-root:not(.MuiDrawer-root *):not(.qqq-branded-header-bar *):not(.MuiButtonBase-root)
|
||||
{
|
||||
color: var(--qqq-link-color, var(--qqq-secondary-color)) !important;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Paper/Card theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
.MuiPaper-root:not(.MuiDrawer-paper):not(.MuiMenu-paper):not(.MuiPopover-paper),
|
||||
.MuiCard-root
|
||||
{
|
||||
background-color: var(--qqq-surface-color) !important;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Menu theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
.MuiMenu-paper,
|
||||
.MuiPopover-paper[role="menu"]
|
||||
{
|
||||
background-color: var(--qqq-surface-color) !important;
|
||||
}
|
||||
|
||||
.MuiMenuItem-root
|
||||
{
|
||||
color: var(--qqq-text-primary);
|
||||
}
|
||||
|
||||
.MuiMenuItem-root:hover
|
||||
{
|
||||
background-color: var(--qqq-menu-hover-color, var(--qqq-action-hover));
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Typography theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
.MuiTypography-body1
|
||||
{
|
||||
font-weight: var(--qqq-font-weight-regular, 400) !important;
|
||||
}
|
||||
|
||||
.MuiTypography-body2
|
||||
{
|
||||
font-weight: var(--qqq-font-weight-regular, 400) !important;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** Button theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
.MuiButton-root
|
||||
{
|
||||
font-weight: var(--qqq-typography-button-font-weight, 500) !important;
|
||||
}
|
||||
|
||||
.MuiButton-containedPrimary
|
||||
{
|
||||
background-color: var(--qqq-primary-color) !important;
|
||||
}
|
||||
|
||||
.MuiButton-containedSecondary
|
||||
{
|
||||
background-color: var(--qqq-secondary-color) !important;
|
||||
}
|
||||
|
||||
.MuiButton-outlinedPrimary
|
||||
{
|
||||
border-color: var(--qqq-primary-color) !important;
|
||||
color: var(--qqq-primary-color) !important;
|
||||
}
|
||||
|
||||
.MuiButton-textPrimary
|
||||
{
|
||||
color: var(--qqq-primary-color) !important;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
** DataGrid theming - uses CSS variables from QThemeMetaData
|
||||
*****************************************************************************/
|
||||
.MuiDataGrid-columnHeaders,
|
||||
.MuiDataGrid-columnHeader
|
||||
{
|
||||
background-color: var(--qqq-table-header-background-color) !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-columnHeaderTitle
|
||||
{
|
||||
color: var(--qqq-table-header-text-color) !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-row:hover
|
||||
{
|
||||
background-color: var(--qqq-table-row-hover-color) !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-row.Mui-selected
|
||||
{
|
||||
background-color: var(--qqq-table-row-selected-color) !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-columnSeparator
|
||||
{
|
||||
border-color: var(--qqq-table-border-color) !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-cell
|
||||
{
|
||||
border-color: var(--qqq-table-border-color) !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-root
|
||||
{
|
||||
border-color: var(--qqq-table-border-color) !important;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
||||
import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import {generateTableHeaderId} from "qqq/utils/qqqIdUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
import React from "react";
|
||||
import {Link, NavigateFunction} from "react-router-dom";
|
||||
@@ -311,17 +312,27 @@ export default class DataGridUtils
|
||||
|
||||
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
|
||||
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
|
||||
const dataQqqId = generateTableHeaderId(undefined, field.name, headerName);
|
||||
|
||||
if (showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />;
|
||||
column.renderHeader = (params: GridColumnHeaderParams) => (
|
||||
<Tooltip title={formattedHelpContent}>
|
||||
<div className="MuiDataGrid-columnHeaderTitle" style={{lineHeight: "initial"}}>
|
||||
<div className="MuiDataGrid-columnHeaderTitle" data-qqq-id={dataQqqId} style={{lineHeight: "initial"}}>
|
||||
{headerName}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
column.renderHeader = (params: GridColumnHeaderParams) => (
|
||||
<div className="MuiDataGrid-columnHeaderTitle" data-qqq-id={dataQqqId} style={{lineHeight: "initial"}}>
|
||||
{headerName}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (column);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,701 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createTheme, Theme, ThemeOptions} from "@mui/material";
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {QThemeMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
import borders from "qqq/assets/theme/base/borders";
|
||||
import boxShadows from "qqq/assets/theme/base/boxShadows";
|
||||
import breakpoints from "qqq/assets/theme/base/breakpoints";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import globals from "qqq/assets/theme/base/globals";
|
||||
import typography from "qqq/assets/theme/base/typography";
|
||||
import appBar from "qqq/assets/theme/components/appBar";
|
||||
import avatar from "qqq/assets/theme/components/avatar";
|
||||
import breadcrumbs from "qqq/assets/theme/components/breadcrumbs";
|
||||
import button from "qqq/assets/theme/components/button";
|
||||
import buttonBase from "qqq/assets/theme/components/buttonBase";
|
||||
import card from "qqq/assets/theme/components/card";
|
||||
import cardContent from "qqq/assets/theme/components/card/cardContent";
|
||||
import cardMedia from "qqq/assets/theme/components/card/cardMedia";
|
||||
import container from "qqq/assets/theme/components/container";
|
||||
import dialog from "qqq/assets/theme/components/dialog";
|
||||
import dialogActions from "qqq/assets/theme/components/dialog/dialogActions";
|
||||
import dialogContent from "qqq/assets/theme/components/dialog/dialogContent";
|
||||
import dialogContentText from "qqq/assets/theme/components/dialog/dialogContentText";
|
||||
import dialogTitle from "qqq/assets/theme/components/dialog/dialogTitle";
|
||||
import divider from "qqq/assets/theme/components/divider";
|
||||
import flatpickr from "qqq/assets/theme/components/flatpickr";
|
||||
import autocomplete from "qqq/assets/theme/components/form/autocomplete";
|
||||
import checkbox from "qqq/assets/theme/components/form/checkbox";
|
||||
import formControlLabel from "qqq/assets/theme/components/form/formControlLabel";
|
||||
import formLabel from "qqq/assets/theme/components/form/formLabel";
|
||||
import input from "qqq/assets/theme/components/form/input";
|
||||
import inputLabel from "qqq/assets/theme/components/form/inputLabel";
|
||||
import inputOutlined from "qqq/assets/theme/components/form/inputOutlined";
|
||||
import radio from "qqq/assets/theme/components/form/radio";
|
||||
import select from "qqq/assets/theme/components/form/select";
|
||||
import switchButton from "qqq/assets/theme/components/form/switchButton";
|
||||
import textField from "qqq/assets/theme/components/form/textField";
|
||||
import icon from "qqq/assets/theme/components/icon";
|
||||
import iconButton from "qqq/assets/theme/components/iconButton";
|
||||
import linearProgress from "qqq/assets/theme/components/linearProgress";
|
||||
import link from "qqq/assets/theme/components/link";
|
||||
import list from "qqq/assets/theme/components/list";
|
||||
import listItem from "qqq/assets/theme/components/list/listItem";
|
||||
import listItemText from "qqq/assets/theme/components/list/listItemText";
|
||||
import menu from "qqq/assets/theme/components/menu";
|
||||
import menuItem from "qqq/assets/theme/components/menu/menuItem";
|
||||
import popover from "qqq/assets/theme/components/popover";
|
||||
import sidenav from "qqq/assets/theme/components/sidenav";
|
||||
import slider from "qqq/assets/theme/components/slider";
|
||||
import stepper from "qqq/assets/theme/components/stepper";
|
||||
import step from "qqq/assets/theme/components/stepper/step";
|
||||
import stepConnector from "qqq/assets/theme/components/stepper/stepConnector";
|
||||
import stepIcon from "qqq/assets/theme/components/stepper/stepIcon";
|
||||
import stepLabel from "qqq/assets/theme/components/stepper/stepLabel";
|
||||
import svgIcon from "qqq/assets/theme/components/svgIcon";
|
||||
import tableCell from "qqq/assets/theme/components/table/tableCell";
|
||||
import tableContainer from "qqq/assets/theme/components/table/tableContainer";
|
||||
import tableHead from "qqq/assets/theme/components/table/tableHead";
|
||||
import tabs from "qqq/assets/theme/components/tabs";
|
||||
import tab from "qqq/assets/theme/components/tabs/tab";
|
||||
import tooltip from "qqq/assets/theme/components/tooltip";
|
||||
import boxShadow from "qqq/assets/theme/functions/boxShadow";
|
||||
import hexToRgb from "qqq/assets/theme/functions/hexToRgb";
|
||||
import linearGradient from "qqq/assets/theme/functions/linearGradient";
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
import rgba from "qqq/assets/theme/functions/rgba";
|
||||
import {DEFAULT_THEME} from "./themeUtils";
|
||||
|
||||
/*******************************************************************************
|
||||
** Density spacing multipliers for compact/normal/comfortable modes.
|
||||
*******************************************************************************/
|
||||
const DENSITY_SPACING: Record<string, number> = {
|
||||
compact: 6,
|
||||
normal: 8,
|
||||
comfortable: 10,
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Parse a border radius string (e.g., "8px") to a number.
|
||||
*******************************************************************************/
|
||||
function parseBorderRadius(value: string | undefined): number
|
||||
{
|
||||
if (!value) return 8;
|
||||
const parsed = parseInt(value, 10);
|
||||
return isNaN(parsed) ? 8 : parsed;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Merge provided theme with defaults, filtering out undefined values.
|
||||
*******************************************************************************/
|
||||
function mergeWithDefaults(theme?: QThemeMetaData): QThemeMetaData
|
||||
{
|
||||
const definedValues: Partial<QThemeMetaData> = {};
|
||||
if (theme)
|
||||
{
|
||||
for (const key of Object.keys(theme) as (keyof QThemeMetaData)[])
|
||||
{
|
||||
if (theme[key] !== undefined)
|
||||
{
|
||||
(definedValues as Record<string, unknown>)[key] = theme[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return {...DEFAULT_THEME, ...definedValues} as QThemeMetaData;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build MUI palette from QThemeMetaData.
|
||||
** Extends the base colors with dynamic theme values.
|
||||
**
|
||||
** Uses type assertion because this project extends MUI's palette with custom
|
||||
** properties like 'focus' that aren't in the standard PaletteOptions type.
|
||||
*******************************************************************************/
|
||||
function buildPalette(theme: QThemeMetaData): ThemeOptions["palette"]
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Start with all existing colors from the base colors file //
|
||||
// Then override with dynamic theme values where provided //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const primaryColor = theme.primaryColor || colors.info.main;
|
||||
const secondaryColor = theme.secondaryColor || colors.secondary.main;
|
||||
const errorColor = theme.errorColor || colors.error.main;
|
||||
const warningColor = theme.warningColor || colors.warning.main;
|
||||
const infoColor = theme.infoColor || colors.info.main;
|
||||
const successColor = theme.successColor || colors.success.main;
|
||||
const textPrimary = theme.textPrimary || "#344767";
|
||||
const textSecondary = theme.textSecondary || "#7b809a";
|
||||
const backgroundColor = theme.backgroundColor || colors.background.default;
|
||||
|
||||
return {
|
||||
...colors,
|
||||
primary: {
|
||||
main: primaryColor,
|
||||
focus: primaryColor,
|
||||
},
|
||||
secondary: {
|
||||
main: secondaryColor,
|
||||
focus: secondaryColor,
|
||||
},
|
||||
error: {
|
||||
main: errorColor,
|
||||
focus: errorColor,
|
||||
},
|
||||
warning: {
|
||||
main: warningColor,
|
||||
focus: warningColor,
|
||||
},
|
||||
info: {
|
||||
main: infoColor,
|
||||
focus: infoColor,
|
||||
},
|
||||
success: {
|
||||
main: successColor,
|
||||
focus: successColor,
|
||||
},
|
||||
background: {
|
||||
default: backgroundColor,
|
||||
},
|
||||
text: {
|
||||
main: textSecondary,
|
||||
focus: textSecondary,
|
||||
primary: textPrimary,
|
||||
secondary: textSecondary,
|
||||
},
|
||||
} as ThemeOptions["palette"];
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build MUI typography from QThemeMetaData.
|
||||
** Merges with base typography to preserve custom properties like size/lineHeight
|
||||
** that are used by the component override files.
|
||||
*******************************************************************************/
|
||||
function buildTypography(theme: QThemeMetaData): ThemeOptions["typography"]
|
||||
{
|
||||
const fontFamily = theme.fontFamily || "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Spread base typography to include custom properties (size, lineHeight, //
|
||||
// fontWeightLighter, display variants) needed by component override files //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
return {
|
||||
...typography,
|
||||
fontFamily,
|
||||
fontWeightLight: theme.fontWeightLight || 300,
|
||||
fontWeightRegular: theme.fontWeightRegular || 400,
|
||||
fontWeightMedium: theme.fontWeightMedium || 500,
|
||||
fontWeightBold: theme.fontWeightBold || 700,
|
||||
h1: {
|
||||
fontFamily: theme.headerFontFamily || fontFamily,
|
||||
fontSize: theme.typographyH1FontSize || "3rem",
|
||||
fontWeight: theme.typographyH1FontWeight || 700,
|
||||
lineHeight: theme.typographyH1LineHeight || 1.25,
|
||||
letterSpacing: theme.typographyH1LetterSpacing || "-0.01562em",
|
||||
textTransform: (theme.typographyH1TextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "none",
|
||||
},
|
||||
h2: {
|
||||
fontFamily: theme.headerFontFamily || fontFamily,
|
||||
fontSize: theme.typographyH2FontSize || "2.125rem",
|
||||
fontWeight: theme.typographyH2FontWeight || 700,
|
||||
lineHeight: theme.typographyH2LineHeight || 1.3,
|
||||
letterSpacing: theme.typographyH2LetterSpacing || "-0.00833em",
|
||||
textTransform: (theme.typographyH2TextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "none",
|
||||
},
|
||||
h3: {
|
||||
fontFamily: theme.headerFontFamily || fontFamily,
|
||||
fontSize: theme.typographyH3FontSize || "1.5rem",
|
||||
fontWeight: theme.typographyH3FontWeight || 700,
|
||||
lineHeight: theme.typographyH3LineHeight || 1.375,
|
||||
letterSpacing: theme.typographyH3LetterSpacing || "0em",
|
||||
textTransform: (theme.typographyH3TextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "none",
|
||||
},
|
||||
h4: {
|
||||
fontFamily: theme.headerFontFamily || fontFamily,
|
||||
fontSize: theme.typographyH4FontSize || "1.25rem",
|
||||
fontWeight: theme.typographyH4FontWeight || 700,
|
||||
lineHeight: theme.typographyH4LineHeight || 1.4,
|
||||
letterSpacing: theme.typographyH4LetterSpacing || "0.00735em",
|
||||
textTransform: (theme.typographyH4TextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "none",
|
||||
},
|
||||
h5: {
|
||||
fontFamily: theme.headerFontFamily || fontFamily,
|
||||
fontSize: theme.typographyH5FontSize || "1rem",
|
||||
fontWeight: theme.typographyH5FontWeight || 600,
|
||||
lineHeight: theme.typographyH5LineHeight || 1.5,
|
||||
letterSpacing: theme.typographyH5LetterSpacing || "0em",
|
||||
textTransform: (theme.typographyH5TextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "none",
|
||||
},
|
||||
h6: {
|
||||
fontFamily: theme.headerFontFamily || fontFamily,
|
||||
fontSize: theme.typographyH6FontSize || "0.875rem",
|
||||
fontWeight: theme.typographyH6FontWeight || 600,
|
||||
lineHeight: theme.typographyH6LineHeight || 1.6,
|
||||
letterSpacing: theme.typographyH6LetterSpacing || "0.0075em",
|
||||
textTransform: (theme.typographyH6TextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "none",
|
||||
},
|
||||
body1: {
|
||||
fontFamily,
|
||||
fontSize: theme.typographyBody1FontSize || "1rem",
|
||||
fontWeight: theme.typographyBody1FontWeight || 400,
|
||||
lineHeight: theme.typographyBody1LineHeight || 1.5,
|
||||
letterSpacing: theme.typographyBody1LetterSpacing || "0.00938em",
|
||||
},
|
||||
body2: {
|
||||
fontFamily,
|
||||
fontSize: theme.typographyBody2FontSize || "0.875rem",
|
||||
fontWeight: theme.typographyBody2FontWeight || 400,
|
||||
lineHeight: theme.typographyBody2LineHeight || 1.43,
|
||||
letterSpacing: theme.typographyBody2LetterSpacing || "0.01071em",
|
||||
},
|
||||
button: {
|
||||
fontFamily,
|
||||
fontSize: theme.typographyButtonFontSize || "0.875rem",
|
||||
fontWeight: theme.typographyButtonFontWeight || 500,
|
||||
lineHeight: theme.typographyButtonLineHeight || 1.75,
|
||||
letterSpacing: theme.typographyButtonLetterSpacing || "0.02857em",
|
||||
textTransform: (theme.typographyButtonTextTransform as "none" | "uppercase" | "lowercase" | "capitalize") || "uppercase",
|
||||
},
|
||||
caption: {
|
||||
fontFamily,
|
||||
fontSize: theme.typographyCaptionFontSize || "0.75rem",
|
||||
fontWeight: theme.typographyCaptionFontWeight || 400,
|
||||
lineHeight: theme.typographyCaptionLineHeight || 1.66,
|
||||
letterSpacing: theme.typographyCaptionLetterSpacing || "0.03333em",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Deep merge helper - merges objects recursively.
|
||||
** Used to combine base component overrides with dynamic theme values.
|
||||
*******************************************************************************/
|
||||
function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T
|
||||
{
|
||||
const output = {...target} as Record<string, unknown>;
|
||||
for (const key of Object.keys(source))
|
||||
{
|
||||
const sourceVal = source[key];
|
||||
const targetVal = target[key];
|
||||
if (
|
||||
sourceVal &&
|
||||
typeof sourceVal === "object" &&
|
||||
!Array.isArray(sourceVal) &&
|
||||
targetVal &&
|
||||
typeof targetVal === "object" &&
|
||||
!Array.isArray(targetVal)
|
||||
)
|
||||
{
|
||||
output[key] = deepMerge(
|
||||
targetVal as Record<string, unknown>,
|
||||
sourceVal as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
else if (sourceVal !== undefined)
|
||||
{
|
||||
output[key] = sourceVal;
|
||||
}
|
||||
}
|
||||
return output as T;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Build MUI component overrides from QThemeMetaData.
|
||||
**
|
||||
** Strategy: Import all 55 component override files as base styling, then apply
|
||||
** dynamic theme values on top. This preserves the detailed styling work while
|
||||
** allowing colors, spacing, and border-radius to be customized.
|
||||
*******************************************************************************/
|
||||
function buildComponents(theme: QThemeMetaData): ThemeOptions["components"]
|
||||
{
|
||||
const borderRadius = parseBorderRadius(theme.borderRadius);
|
||||
const density = theme.density || "normal";
|
||||
const spacingUnit = DENSITY_SPACING[density] || 8;
|
||||
|
||||
const primaryColor = theme.primaryColor || "#0062FF";
|
||||
const textPrimary = theme.textPrimary || "#344767";
|
||||
const textSecondary = theme.textSecondary || "#7b809a";
|
||||
const backgroundColor = theme.backgroundColor || "#f0f2f5";
|
||||
const surfaceColor = theme.surfaceColor || "#ffffff";
|
||||
const borderColor = theme.borderColor || "rgba(0, 0, 0, 0.12)";
|
||||
const dividerColor = theme.dividerColor || "rgba(0, 0, 0, 0.12)";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Base components from the 55 static files - preserves all detailed //
|
||||
// styling like shadows, transitions, sizes, etc. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const baseComponents: ThemeOptions["components"] = {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
...globals,
|
||||
...flatpickr,
|
||||
...container,
|
||||
},
|
||||
},
|
||||
MuiDrawer: {...sidenav},
|
||||
MuiList: {...list},
|
||||
MuiListItem: {...listItem},
|
||||
MuiListItemText: {...listItemText},
|
||||
MuiCard: {...card},
|
||||
MuiCardMedia: {...cardMedia},
|
||||
MuiCardContent: {...cardContent},
|
||||
MuiButton: {...button},
|
||||
MuiIconButton: {...iconButton},
|
||||
MuiInput: {...input},
|
||||
MuiInputLabel: {...inputLabel},
|
||||
MuiOutlinedInput: {...inputOutlined},
|
||||
MuiTextField: {...textField},
|
||||
MuiMenu: {...menu},
|
||||
MuiMenuItem: {...menuItem},
|
||||
MuiSwitch: {...switchButton},
|
||||
MuiDivider: {...divider},
|
||||
MuiTableContainer: {...tableContainer},
|
||||
MuiTableHead: {...tableHead},
|
||||
MuiTableCell: {...tableCell},
|
||||
MuiLinearProgress: {...linearProgress},
|
||||
MuiBreadcrumbs: {...breadcrumbs},
|
||||
MuiSlider: {...slider},
|
||||
MuiAvatar: {...avatar},
|
||||
MuiTooltip: {...tooltip},
|
||||
MuiAppBar: {...appBar},
|
||||
MuiTabs: {...tabs},
|
||||
MuiTab: {...tab},
|
||||
MuiStepper: {...stepper},
|
||||
MuiStep: {...step},
|
||||
MuiStepConnector: {...stepConnector},
|
||||
MuiStepLabel: {...stepLabel},
|
||||
MuiStepIcon: {...stepIcon},
|
||||
MuiSelect: {...select},
|
||||
MuiFormControlLabel: {...formControlLabel},
|
||||
MuiFormLabel: {...formLabel},
|
||||
MuiCheckbox: {...checkbox},
|
||||
MuiRadio: {...radio},
|
||||
MuiAutocomplete: {...autocomplete},
|
||||
MuiPopover: {...popover},
|
||||
MuiButtonBase: {...buttonBase},
|
||||
MuiIcon: {...icon},
|
||||
MuiSvgIcon: {...svgIcon},
|
||||
MuiLink: {...link},
|
||||
MuiDialog: {...dialog},
|
||||
MuiDialogTitle: {...dialogTitle},
|
||||
MuiDialogContent: {...dialogContent},
|
||||
MuiDialogContentText: {...dialogContentText},
|
||||
MuiDialogActions: {...dialogActions},
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Dynamic overrides - theme-specific values that override the base //
|
||||
// These are the properties that should change based on QThemeMetaData //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const dynamicOverrides: ThemeOptions["components"] = {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
backgroundColor,
|
||||
},
|
||||
"a.link, .link, a.link:link, .link:link, a.link:visited, .link:visited": {
|
||||
color: `${primaryColor} !important`,
|
||||
},
|
||||
"a.link:hover, .link:hover, a.link:focus, .link:focus": {
|
||||
color: `${primaryColor} !important`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
padding: `${spacingUnit}px ${spacingUnit * 2}px`,
|
||||
},
|
||||
containedPrimary: {
|
||||
backgroundColor: primaryColor,
|
||||
"&:hover": {
|
||||
backgroundColor: primaryColor,
|
||||
filter: "brightness(0.9)",
|
||||
},
|
||||
},
|
||||
outlinedPrimary: {
|
||||
borderColor: primaryColor,
|
||||
color: primaryColor,
|
||||
"&:hover": {
|
||||
backgroundColor: `${primaryColor}10`,
|
||||
},
|
||||
},
|
||||
textPrimary: {
|
||||
color: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: `${spacingUnit * 2}px`,
|
||||
"&:last-child": {
|
||||
paddingBottom: `${spacingUnit * 2}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: `${borderRadius / 2}px`,
|
||||
},
|
||||
colorPrimary: {
|
||||
backgroundColor: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDivider: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: dividerColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: textSecondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: primaryColor,
|
||||
},
|
||||
},
|
||||
notchedOutline: {
|
||||
borderColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputLabel: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: textSecondary,
|
||||
"&.Mui-focused": {
|
||||
color: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLinearProgress: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: `${borderRadius / 2}px`,
|
||||
},
|
||||
bar: {
|
||||
borderRadius: `${borderRadius / 2}px`,
|
||||
},
|
||||
colorPrimary: {
|
||||
backgroundColor: `${primaryColor}30`,
|
||||
},
|
||||
barColorPrimary: {
|
||||
backgroundColor: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenu: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: `${spacingUnit}px ${spacingUnit * 2}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: surfaceColor,
|
||||
},
|
||||
rounded: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPopover: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSwitch: {
|
||||
styleOverrides: {
|
||||
colorPrimary: {
|
||||
"&.Mui-checked": {
|
||||
color: primaryColor,
|
||||
},
|
||||
"&.Mui-checked + .MuiSwitch-track": {
|
||||
backgroundColor: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: textSecondary,
|
||||
"&.Mui-selected": {
|
||||
color: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
indicator: {
|
||||
backgroundColor: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderBottom: `1px solid ${theme.tableBorderColor || borderColor}`,
|
||||
padding: `${spacingUnit}px ${spacingUnit * 2}px`,
|
||||
},
|
||||
head: {
|
||||
backgroundColor: theme.tableHeaderBackgroundColor || backgroundColor,
|
||||
color: theme.tableHeaderTextColor || textPrimary,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:hover": {
|
||||
backgroundColor: theme.tableRowHoverColor || "rgba(0, 0, 0, 0.04)",
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.tableRowSelectedColor || "rgba(0, 98, 255, 0.08)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
borderRadius: `${borderRadius / 2}px`,
|
||||
padding: `${spacingUnit / 2}px ${spacingUnit}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: textPrimary,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Merge base components with dynamic overrides //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
return deepMerge(
|
||||
baseComponents as Record<string, unknown>,
|
||||
dynamicOverrides as Record<string, unknown>
|
||||
) as ThemeOptions["components"];
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Create a complete MUI theme from QThemeMetaData.
|
||||
**
|
||||
** This is the core of the MUI-first theming approach. Instead of injecting
|
||||
** CSS variables and having MUI read them, we build the theme object directly
|
||||
** from the QThemeMetaData values.
|
||||
**
|
||||
** The theme is built in three parts:
|
||||
** 1. palette - colors for primary, secondary, error, etc.
|
||||
** 2. typography - font families, sizes, weights for all variants
|
||||
** 3. components - style overrides for MUI components
|
||||
*******************************************************************************/
|
||||
export function createDynamicTheme(themeMetaData?: QThemeMetaData): Theme
|
||||
{
|
||||
const mergedTheme = mergeWithDefaults(themeMetaData);
|
||||
|
||||
const density = mergedTheme.density || "normal";
|
||||
const spacingUnit = DENSITY_SPACING[density] || 8;
|
||||
|
||||
const themeOptions: ThemeOptions = {
|
||||
breakpoints: {...breakpoints},
|
||||
palette: buildPalette(mergedTheme),
|
||||
typography: buildTypography(mergedTheme),
|
||||
spacing: spacingUnit,
|
||||
shape: {
|
||||
borderRadius: parseBorderRadius(mergedTheme.borderRadius),
|
||||
},
|
||||
boxShadows: {...boxShadows},
|
||||
borders: {...borders},
|
||||
functions: {
|
||||
boxShadow,
|
||||
hexToRgb,
|
||||
linearGradient,
|
||||
pxToRem,
|
||||
rgba,
|
||||
},
|
||||
components: buildComponents(mergedTheme),
|
||||
};
|
||||
|
||||
return createTheme(themeOptions);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Export the merged theme data for use by island CSS variable injection.
|
||||
** This allows components like sidebar and branded header to access theme values.
|
||||
*******************************************************************************/
|
||||
export function getMergedTheme(themeMetaData?: QThemeMetaData): QThemeMetaData
|
||||
{
|
||||
return mergeWithDefaults(themeMetaData);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {QThemeMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
import {DEFAULT_THEME} from "./themeUtils";
|
||||
|
||||
/*******************************************************************************
|
||||
** CSS variable prefix for QQQ island variables.
|
||||
*******************************************************************************/
|
||||
const CSS_VAR_PREFIX = "--qqq-";
|
||||
|
||||
/*******************************************************************************
|
||||
** Inject CSS variables for "island" components that don't use MUI palette.
|
||||
**
|
||||
** Islands are components with their own theming system:
|
||||
** - Sidebar: Dark theme with its own color scheme
|
||||
** - Branded Header: Custom header bar with logo/tagline
|
||||
** - DataGrid: MUI X component with specific styling needs
|
||||
**
|
||||
** Unlike MUI components (which get colors from theme.palette), these islands
|
||||
** need CSS variables to be styled consistently with the rest of the app.
|
||||
*******************************************************************************/
|
||||
export function injectIslandVariables(theme?: QThemeMetaData): void
|
||||
{
|
||||
const root = document.documentElement;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Merge with defaults, filtering out undefined values //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const definedValues: Partial<QThemeMetaData> = {};
|
||||
if (theme)
|
||||
{
|
||||
for (const key of Object.keys(theme) as (keyof QThemeMetaData)[])
|
||||
{
|
||||
if (theme[key] !== undefined)
|
||||
{
|
||||
(definedValues as Record<string, unknown>)[key] = theme[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
const mergedTheme = {...DEFAULT_THEME, ...definedValues} as QThemeMetaData;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Sidebar variables //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "sidebar-background-color", mergedTheme.sidebarBackgroundColor);
|
||||
setVar(root, "sidebar-text-color", mergedTheme.sidebarTextColor);
|
||||
setVar(root, "sidebar-icon-color", mergedTheme.sidebarIconColor);
|
||||
setVar(root, "sidebar-selected-background-color", mergedTheme.sidebarSelectedBackgroundColor);
|
||||
setVar(root, "sidebar-selected-text-color", mergedTheme.sidebarSelectedTextColor);
|
||||
setVar(root, "sidebar-hover-background-color", mergedTheme.sidebarHoverBackgroundColor);
|
||||
setVar(root, "sidebar-divider-color", mergedTheme.sidebarDividerColor);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Branded Header variables //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "branded-header-enabled", mergedTheme.brandedHeaderEnabled ? "true" : "false");
|
||||
setVar(root, "branded-header-background-color", mergedTheme.brandedHeaderBackgroundColor);
|
||||
setVar(root, "branded-header-text-color", mergedTheme.brandedHeaderTextColor);
|
||||
setVar(root, "branded-header-height", mergedTheme.brandedHeaderHeight);
|
||||
setVar(root, "branded-header-logo-path", mergedTheme.brandedHeaderLogoPath);
|
||||
setVar(root, "branded-header-logo-alt-text", mergedTheme.brandedHeaderLogoAltText);
|
||||
setVar(root, "branded-header-tagline", mergedTheme.brandedHeaderTagline);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// DataGrid variables (table-specific styling) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "table-header-background-color", mergedTheme.tableHeaderBackgroundColor);
|
||||
setVar(root, "table-header-text-color", mergedTheme.tableHeaderTextColor);
|
||||
setVar(root, "table-row-hover-color", mergedTheme.tableRowHoverColor);
|
||||
setVar(root, "table-row-selected-color", mergedTheme.tableRowSelectedColor);
|
||||
setVar(root, "table-border-color", mergedTheme.tableBorderColor);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// General variables needed by islands //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "primary-color", mergedTheme.primaryColor);
|
||||
setVar(root, "secondary-color", mergedTheme.secondaryColor);
|
||||
setVar(root, "text-primary", mergedTheme.textPrimary);
|
||||
setVar(root, "text-secondary", mergedTheme.textSecondary);
|
||||
setVar(root, "background-color", mergedTheme.backgroundColor);
|
||||
setVar(root, "surface-color", mergedTheme.surfaceColor);
|
||||
setVar(root, "border-color", mergedTheme.borderColor);
|
||||
setVar(root, "card-border-color", mergedTheme.cardBorderColor);
|
||||
setVar(root, "divider-color", mergedTheme.dividerColor);
|
||||
setVar(root, "border-radius", mergedTheme.borderRadius);
|
||||
setVar(root, "error-color", mergedTheme.errorColor);
|
||||
setVar(root, "warning-color", mergedTheme.warningColor);
|
||||
setVar(root, "success-color", mergedTheme.successColor);
|
||||
setVar(root, "info-color", mergedTheme.infoColor);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Typography base variables //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "font-family", mergedTheme.fontFamily);
|
||||
setVar(root, "header-font-family", mergedTheme.headerFontFamily);
|
||||
setVar(root, "mono-font-family", mergedTheme.monoFontFamily);
|
||||
setVar(root, "font-size-base", mergedTheme.fontSizeBase);
|
||||
setVar(root, "font-weight-light", mergedTheme.fontWeightLight);
|
||||
setVar(root, "font-weight-regular", mergedTheme.fontWeightRegular);
|
||||
setVar(root, "font-weight-medium", mergedTheme.fontWeightMedium);
|
||||
setVar(root, "font-weight-bold", mergedTheme.fontWeightBold);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Typography variant variables (H1-H6, Body1, Body2, Button, Caption) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "typography-h1-font-size", mergedTheme.typographyH1FontSize);
|
||||
setVar(root, "typography-h1-font-weight", mergedTheme.typographyH1FontWeight);
|
||||
setVar(root, "typography-h1-line-height", mergedTheme.typographyH1LineHeight);
|
||||
setVar(root, "typography-h1-letter-spacing", mergedTheme.typographyH1LetterSpacing);
|
||||
setVar(root, "typography-h1-text-transform", mergedTheme.typographyH1TextTransform);
|
||||
|
||||
setVar(root, "typography-h2-font-size", mergedTheme.typographyH2FontSize);
|
||||
setVar(root, "typography-h2-font-weight", mergedTheme.typographyH2FontWeight);
|
||||
setVar(root, "typography-h2-line-height", mergedTheme.typographyH2LineHeight);
|
||||
setVar(root, "typography-h2-letter-spacing", mergedTheme.typographyH2LetterSpacing);
|
||||
setVar(root, "typography-h2-text-transform", mergedTheme.typographyH2TextTransform);
|
||||
|
||||
setVar(root, "typography-h3-font-size", mergedTheme.typographyH3FontSize);
|
||||
setVar(root, "typography-h3-font-weight", mergedTheme.typographyH3FontWeight);
|
||||
setVar(root, "typography-h3-line-height", mergedTheme.typographyH3LineHeight);
|
||||
setVar(root, "typography-h3-letter-spacing", mergedTheme.typographyH3LetterSpacing);
|
||||
setVar(root, "typography-h3-text-transform", mergedTheme.typographyH3TextTransform);
|
||||
|
||||
setVar(root, "typography-h4-font-size", mergedTheme.typographyH4FontSize);
|
||||
setVar(root, "typography-h4-font-weight", mergedTheme.typographyH4FontWeight);
|
||||
setVar(root, "typography-h4-line-height", mergedTheme.typographyH4LineHeight);
|
||||
setVar(root, "typography-h4-letter-spacing", mergedTheme.typographyH4LetterSpacing);
|
||||
setVar(root, "typography-h4-text-transform", mergedTheme.typographyH4TextTransform);
|
||||
|
||||
setVar(root, "typography-h5-font-size", mergedTheme.typographyH5FontSize);
|
||||
setVar(root, "typography-h5-font-weight", mergedTheme.typographyH5FontWeight);
|
||||
setVar(root, "typography-h5-line-height", mergedTheme.typographyH5LineHeight);
|
||||
setVar(root, "typography-h5-letter-spacing", mergedTheme.typographyH5LetterSpacing);
|
||||
setVar(root, "typography-h5-text-transform", mergedTheme.typographyH5TextTransform);
|
||||
|
||||
setVar(root, "typography-h6-font-size", mergedTheme.typographyH6FontSize);
|
||||
setVar(root, "typography-h6-font-weight", mergedTheme.typographyH6FontWeight);
|
||||
setVar(root, "typography-h6-line-height", mergedTheme.typographyH6LineHeight);
|
||||
setVar(root, "typography-h6-letter-spacing", mergedTheme.typographyH6LetterSpacing);
|
||||
setVar(root, "typography-h6-text-transform", mergedTheme.typographyH6TextTransform);
|
||||
|
||||
setVar(root, "typography-body1-font-size", mergedTheme.typographyBody1FontSize);
|
||||
setVar(root, "typography-body1-font-weight", mergedTheme.typographyBody1FontWeight);
|
||||
setVar(root, "typography-body1-line-height", mergedTheme.typographyBody1LineHeight);
|
||||
setVar(root, "typography-body1-letter-spacing", mergedTheme.typographyBody1LetterSpacing);
|
||||
setVar(root, "typography-body1-text-transform", mergedTheme.typographyBody1TextTransform);
|
||||
|
||||
setVar(root, "typography-body2-font-size", mergedTheme.typographyBody2FontSize);
|
||||
setVar(root, "typography-body2-font-weight", mergedTheme.typographyBody2FontWeight);
|
||||
setVar(root, "typography-body2-line-height", mergedTheme.typographyBody2LineHeight);
|
||||
setVar(root, "typography-body2-letter-spacing", mergedTheme.typographyBody2LetterSpacing);
|
||||
setVar(root, "typography-body2-text-transform", mergedTheme.typographyBody2TextTransform);
|
||||
|
||||
setVar(root, "typography-button-font-size", mergedTheme.typographyButtonFontSize);
|
||||
setVar(root, "typography-button-font-weight", mergedTheme.typographyButtonFontWeight);
|
||||
setVar(root, "typography-button-line-height", mergedTheme.typographyButtonLineHeight);
|
||||
setVar(root, "typography-button-letter-spacing", mergedTheme.typographyButtonLetterSpacing);
|
||||
setVar(root, "typography-button-text-transform", mergedTheme.typographyButtonTextTransform);
|
||||
|
||||
setVar(root, "typography-caption-font-size", mergedTheme.typographyCaptionFontSize);
|
||||
setVar(root, "typography-caption-font-weight", mergedTheme.typographyCaptionFontWeight);
|
||||
setVar(root, "typography-caption-line-height", mergedTheme.typographyCaptionLineHeight);
|
||||
setVar(root, "typography-caption-letter-spacing", mergedTheme.typographyCaptionLetterSpacing);
|
||||
setVar(root, "typography-caption-text-transform", mergedTheme.typographyCaptionTextTransform);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Density and spacing variables //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const density = mergedTheme.density || "normal";
|
||||
const spacing = DENSITY_SPACING[density] || DENSITY_SPACING.normal;
|
||||
setVar(root, "density", density);
|
||||
setVar(root, "spacing-base", `${spacing.base}px`);
|
||||
setVar(root, "spacing-small", spacing.small);
|
||||
setVar(root, "spacing-medium", spacing.medium);
|
||||
setVar(root, "spacing-large", spacing.large);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// App Bar variables //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "app-bar-background-color", mergedTheme.appBarBackgroundColor);
|
||||
setVar(root, "app-bar-text-color", mergedTheme.appBarTextColor);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Asset paths //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "logo-path", mergedTheme.logoPath);
|
||||
setVar(root, "icon-path", mergedTheme.iconPath);
|
||||
setVar(root, "favicon-path", mergedTheme.faviconPath);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Icon style for MUI Icon baseClassName //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
setVar(root, "icon-style", mergedTheme.iconStyle || "filled");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Inject icon fonts //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
injectIconFonts();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Inject custom CSS if provided //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
if (mergedTheme.customCss)
|
||||
{
|
||||
injectCustomCss(mergedTheme.customCss);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Helper to set a CSS variable on an element.
|
||||
*******************************************************************************/
|
||||
function setVar(element: HTMLElement, name: string, value: string | number | boolean | undefined): void
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
element.style.setProperty(`${CSS_VAR_PREFIX}${name}`, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Density spacing multipliers.
|
||||
*******************************************************************************/
|
||||
const DENSITY_SPACING: Record<string, { base: number; small: string; medium: string; large: string }> = {
|
||||
compact: {base: 6, small: "0.25rem", medium: "0.5rem", large: "0.75rem"},
|
||||
normal: {base: 8, small: "0.5rem", medium: "1rem", large: "1.5rem"},
|
||||
comfortable: {base: 10, small: "0.75rem", medium: "1.25rem", large: "2rem"},
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Default font CDN URLs.
|
||||
*******************************************************************************/
|
||||
const DEFAULT_ICON_FONT_URL = "https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Two+Tone|Material+Icons+Round|Material+Icons+Sharp";
|
||||
const DEFAULT_FONT_URL = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap";
|
||||
|
||||
/*******************************************************************************
|
||||
** Inject font stylesheets.
|
||||
*******************************************************************************/
|
||||
function injectIconFonts(): void
|
||||
{
|
||||
const iconFontLinkId = "qqq-icon-font";
|
||||
const fontLinkId = "qqq-font";
|
||||
|
||||
if (!document.getElementById(fontLinkId))
|
||||
{
|
||||
const fontLink = document.createElement("link");
|
||||
fontLink.id = fontLinkId;
|
||||
fontLink.rel = "stylesheet";
|
||||
fontLink.href = DEFAULT_FONT_URL;
|
||||
document.head.appendChild(fontLink);
|
||||
}
|
||||
|
||||
if (!document.getElementById(iconFontLinkId))
|
||||
{
|
||||
const iconLink = document.createElement("link");
|
||||
iconLink.id = iconFontLinkId;
|
||||
iconLink.rel = "stylesheet";
|
||||
iconLink.href = DEFAULT_ICON_FONT_URL;
|
||||
document.head.appendChild(iconLink);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Inject custom CSS into the document head.
|
||||
*******************************************************************************/
|
||||
function injectCustomCss(css: string): void
|
||||
{
|
||||
const existingStyle = document.getElementById("qqq-custom-theme-css");
|
||||
if (existingStyle)
|
||||
{
|
||||
existingStyle.textContent = css;
|
||||
}
|
||||
else
|
||||
{
|
||||
const style = document.createElement("style");
|
||||
style.id = "qqq-custom-theme-css";
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Map icon style name to MUI Icon baseClassName.
|
||||
*******************************************************************************/
|
||||
const ICON_STYLE_TO_CLASS: Record<string, string> = {
|
||||
filled: "material-icons",
|
||||
outlined: "material-icons-outlined",
|
||||
rounded: "material-icons-round",
|
||||
sharp: "material-icons-sharp",
|
||||
"two-tone": "material-icons-two-tone",
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the MUI Icon baseClassName for the current theme's iconStyle.
|
||||
*******************************************************************************/
|
||||
export function getIconBaseClassName(): string
|
||||
{
|
||||
const iconStyle = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--qqq-icon-style").trim() || "filled";
|
||||
return ICON_STYLE_TO_CLASS[iconStyle] || ICON_STYLE_TO_CLASS.filled;
|
||||
}
|
||||
@@ -35,8 +35,14 @@ import QContext from "QContext";
|
||||
import QDynamicForm from "qqq/components/forms/DynamicForm";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import MDButton from "qqq/components/legacy/MDButton";
|
||||
import theme from "qqq/components/legacy/Theme";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
import {createDynamicTheme} from "qqq/utils/createDynamicTheme";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Static theme instance for bridge components that don't have access to //
|
||||
// runtime theme metadata from the main app. Uses default theme values. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const bridgeTheme = createDynamicTheme();
|
||||
import {MaterialUIControllerProvider} from "qqq/context";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {ReactElement, ReactNode, useContext, useEffect, useState} from "react";
|
||||
@@ -168,7 +174,7 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit, helpRoles,
|
||||
return (
|
||||
<QContext.Provider value={mergedContext}>
|
||||
<MaterialUIControllerProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={bridgeTheme}>
|
||||
<Formik initialValues={initialValues} validationSchema={Yup.object().shape(formValidations)} onSubmit={handleSubmit}>
|
||||
{({values, errors, touched}) =>
|
||||
{
|
||||
@@ -268,7 +274,7 @@ function QFMDBridgeWidget({widgetName, tableName, record, entityPrimaryKey, acti
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (<BrowserRouter>
|
||||
<MaterialUIControllerProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={bridgeTheme}>
|
||||
<QContext.Provider value={{
|
||||
...(qContextProp ?? qContext),
|
||||
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
|
||||
@@ -388,7 +394,7 @@ function QFMDBridgeButton({label, onClick, extra}: QFMDBridgeButtonProps): JSX.E
|
||||
{
|
||||
return (
|
||||
<MaterialUIControllerProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={bridgeTheme}>
|
||||
<MDButton {...extra} onClick={onClick} fullWidth>{label}</MDButton>
|
||||
</ThemeProvider>
|
||||
</MaterialUIControllerProvider>
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Children, isValidElement, ReactNode} from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Sanitize a string for use as a data-qqq-id attribute value.
|
||||
** Converts to lowercase, replaces non-alphanumeric characters with dashes,
|
||||
** and limits length.
|
||||
*******************************************************************************/
|
||||
export function sanitizeId(text: string): string
|
||||
{
|
||||
if (!text)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.substring(0, 50);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Extract text content from React children (handles strings, nested elements).
|
||||
*******************************************************************************/
|
||||
export function extractTextFromChildren(children: ReactNode): string
|
||||
{
|
||||
if (typeof children === "string")
|
||||
{
|
||||
return children;
|
||||
}
|
||||
|
||||
if (typeof children === "number")
|
||||
{
|
||||
return String(children);
|
||||
}
|
||||
|
||||
if (Array.isArray(children))
|
||||
{
|
||||
return children.map(extractTextFromChildren).filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
if (isValidElement(children))
|
||||
{
|
||||
const props = children.props as {children?: ReactNode};
|
||||
if (props.children)
|
||||
{
|
||||
return extractTextFromChildren(props.children);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a button element.
|
||||
** Priority: explicit qqqId > text content > icon name > fallback
|
||||
*******************************************************************************/
|
||||
export function generateButtonId(
|
||||
qqqId?: string,
|
||||
children?: ReactNode,
|
||||
iconName?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `button-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
const textContent = children ? extractTextFromChildren(children) : "";
|
||||
if (textContent)
|
||||
{
|
||||
return `button-${sanitizeId(textContent)}`;
|
||||
}
|
||||
|
||||
if (iconName)
|
||||
{
|
||||
return `button-icon-${sanitizeId(iconName)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for an input/text field element.
|
||||
** Priority: explicit qqqId > field name > label
|
||||
*******************************************************************************/
|
||||
export function generateInputId(
|
||||
qqqId?: string,
|
||||
fieldName?: string,
|
||||
label?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `input-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (fieldName)
|
||||
{
|
||||
return `input-${sanitizeId(fieldName)}`;
|
||||
}
|
||||
|
||||
if (label)
|
||||
{
|
||||
return `input-${sanitizeId(label)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a select/dropdown element.
|
||||
*******************************************************************************/
|
||||
export function generateSelectId(
|
||||
qqqId?: string,
|
||||
fieldName?: string,
|
||||
label?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `select-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (fieldName)
|
||||
{
|
||||
return `select-${sanitizeId(fieldName)}`;
|
||||
}
|
||||
|
||||
if (label)
|
||||
{
|
||||
return `select-${sanitizeId(label)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a switch/checkbox/toggle element.
|
||||
*******************************************************************************/
|
||||
export function generateSwitchId(
|
||||
qqqId?: string,
|
||||
fieldName?: string,
|
||||
label?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `switch-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (fieldName)
|
||||
{
|
||||
return `switch-${sanitizeId(fieldName)}`;
|
||||
}
|
||||
|
||||
if (label)
|
||||
{
|
||||
return `switch-${sanitizeId(label)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a navigation/sidebar item.
|
||||
*******************************************************************************/
|
||||
export function generateNavItemId(
|
||||
qqqId?: string,
|
||||
name?: string,
|
||||
route?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `sidenav-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (name)
|
||||
{
|
||||
return `sidenav-${sanitizeId(name)}`;
|
||||
}
|
||||
|
||||
if (route)
|
||||
{
|
||||
const routeName = route.split("/").filter(Boolean).pop() || "";
|
||||
return `sidenav-${sanitizeId(routeName)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a menu item.
|
||||
*******************************************************************************/
|
||||
export function generateMenuItemId(
|
||||
qqqId?: string,
|
||||
text?: string,
|
||||
index?: number
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `menu-item-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (text)
|
||||
{
|
||||
return `menu-item-${sanitizeId(text)}`;
|
||||
}
|
||||
|
||||
if (index !== undefined)
|
||||
{
|
||||
return `menu-item-${index}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a tab element.
|
||||
*******************************************************************************/
|
||||
export function generateTabId(
|
||||
qqqId?: string,
|
||||
label?: string,
|
||||
index?: number
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `tab-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (label)
|
||||
{
|
||||
return `tab-${sanitizeId(label)}`;
|
||||
}
|
||||
|
||||
if (index !== undefined)
|
||||
{
|
||||
return `tab-${index}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a table header/column.
|
||||
*******************************************************************************/
|
||||
export function generateTableHeaderId(
|
||||
qqqId?: string,
|
||||
fieldName?: string,
|
||||
headerText?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `table-header-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
if (fieldName)
|
||||
{
|
||||
return `table-header-${sanitizeId(fieldName)}`;
|
||||
}
|
||||
|
||||
if (headerText)
|
||||
{
|
||||
return `table-header-${sanitizeId(headerText)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Generate a data-qqq-id for a link element.
|
||||
*******************************************************************************/
|
||||
export function generateLinkId(
|
||||
qqqId?: string,
|
||||
children?: ReactNode,
|
||||
href?: string
|
||||
): string | undefined
|
||||
{
|
||||
if (qqqId)
|
||||
{
|
||||
return `link-${sanitizeId(qqqId)}`;
|
||||
}
|
||||
|
||||
const textContent = children ? extractTextFromChildren(children) : "";
|
||||
if (textContent)
|
||||
{
|
||||
return `link-${sanitizeId(textContent)}`;
|
||||
}
|
||||
|
||||
if (href)
|
||||
{
|
||||
const hrefName = href.split("/").filter(Boolean).pop() || "";
|
||||
return `link-${sanitizeId(hrefName)}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {QThemeMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
|
||||
|
||||
/*******************************************************************************
|
||||
** Default theme values matching the current QQQ color scheme.
|
||||
*******************************************************************************/
|
||||
export const DEFAULT_THEME: QThemeMetaData = {
|
||||
// Color palette
|
||||
primaryColor: "#0062FF",
|
||||
secondaryColor: "#7b809a",
|
||||
backgroundColor: "#f0f2f5",
|
||||
surfaceColor: "#ffffff",
|
||||
textPrimary: "#344767",
|
||||
textSecondary: "#7b809a",
|
||||
errorColor: "#F44335",
|
||||
warningColor: "#fb8c00",
|
||||
successColor: "#4CAF50",
|
||||
infoColor: "#0062FF",
|
||||
|
||||
// Typography - Base
|
||||
fontFamily: "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
|
||||
headerFontFamily: "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
|
||||
monoFontFamily: "\"Roboto Mono\", \"Courier New\", monospace",
|
||||
fontSizeBase: "14px",
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
fontWeightMedium: 500,
|
||||
fontWeightBold: 700,
|
||||
|
||||
// Typography - H1
|
||||
typographyH1FontSize: "3rem",
|
||||
typographyH1FontWeight: 700,
|
||||
typographyH1LineHeight: 1.25,
|
||||
typographyH1LetterSpacing: "-0.01562em",
|
||||
typographyH1TextTransform: "none",
|
||||
|
||||
// Typography - H2
|
||||
typographyH2FontSize: "2.125rem",
|
||||
typographyH2FontWeight: 700,
|
||||
typographyH2LineHeight: 1.3,
|
||||
typographyH2LetterSpacing: "-0.00833em",
|
||||
typographyH2TextTransform: "none",
|
||||
|
||||
// Typography - H3
|
||||
typographyH3FontSize: "1.5rem",
|
||||
typographyH3FontWeight: 700,
|
||||
typographyH3LineHeight: 1.375,
|
||||
typographyH3LetterSpacing: "0em",
|
||||
typographyH3TextTransform: "none",
|
||||
|
||||
// Typography - H4
|
||||
typographyH4FontSize: "1.25rem",
|
||||
typographyH4FontWeight: 700,
|
||||
typographyH4LineHeight: 1.4,
|
||||
typographyH4LetterSpacing: "0.00735em",
|
||||
typographyH4TextTransform: "none",
|
||||
|
||||
// Typography - H5
|
||||
typographyH5FontSize: "1rem",
|
||||
typographyH5FontWeight: 600,
|
||||
typographyH5LineHeight: 1.5,
|
||||
typographyH5LetterSpacing: "0em",
|
||||
typographyH5TextTransform: "none",
|
||||
|
||||
// Typography - H6
|
||||
typographyH6FontSize: "0.875rem",
|
||||
typographyH6FontWeight: 600,
|
||||
typographyH6LineHeight: 1.6,
|
||||
typographyH6LetterSpacing: "0.0075em",
|
||||
typographyH6TextTransform: "none",
|
||||
|
||||
// Typography - Body1
|
||||
typographyBody1FontSize: "1rem",
|
||||
typographyBody1FontWeight: 400,
|
||||
typographyBody1LineHeight: 1.5,
|
||||
typographyBody1LetterSpacing: "0.00938em",
|
||||
typographyBody1TextTransform: "none",
|
||||
|
||||
// Typography - Body2
|
||||
typographyBody2FontSize: "0.875rem",
|
||||
typographyBody2FontWeight: 400,
|
||||
typographyBody2LineHeight: 1.43,
|
||||
typographyBody2LetterSpacing: "0.01071em",
|
||||
typographyBody2TextTransform: "none",
|
||||
|
||||
// Typography - Button
|
||||
typographyButtonFontSize: "0.875rem",
|
||||
typographyButtonFontWeight: 500,
|
||||
typographyButtonLineHeight: 1.75,
|
||||
typographyButtonLetterSpacing: "0.02857em",
|
||||
typographyButtonTextTransform: "uppercase",
|
||||
|
||||
// Typography - Caption
|
||||
typographyCaptionFontSize: "0.75rem",
|
||||
typographyCaptionFontWeight: 400,
|
||||
typographyCaptionLineHeight: 1.66,
|
||||
typographyCaptionLetterSpacing: "0.03333em",
|
||||
typographyCaptionTextTransform: "none",
|
||||
|
||||
// Sizing
|
||||
borderRadius: "8px",
|
||||
density: "normal",
|
||||
|
||||
// Branded Header Bar (disabled by default)
|
||||
brandedHeaderEnabled: false,
|
||||
brandedHeaderBackgroundColor: "#1a2035",
|
||||
brandedHeaderTextColor: "#ffffff",
|
||||
brandedHeaderHeight: "48px",
|
||||
brandedHeaderTagline: "",
|
||||
|
||||
// Sidebar (dark theme defaults matching current QQQ)
|
||||
sidebarBackgroundColor: "#1a2035",
|
||||
sidebarTextColor: "#ffffff",
|
||||
sidebarIconColor: "#ffffff",
|
||||
sidebarSelectedBackgroundColor: "#0062FF",
|
||||
sidebarSelectedTextColor: "#ffffff",
|
||||
sidebarHoverBackgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
sidebarDividerColor: "rgba(255, 255, 255, 0.2)",
|
||||
|
||||
// App Bar
|
||||
appBarBackgroundColor: "#ffffff",
|
||||
appBarTextColor: "#344767",
|
||||
|
||||
// Tables
|
||||
tableHeaderBackgroundColor: "#f0f2f5",
|
||||
tableHeaderTextColor: "#344767",
|
||||
tableRowHoverColor: "rgba(0, 0, 0, 0.04)",
|
||||
tableRowSelectedColor: "rgba(0, 98, 255, 0.08)",
|
||||
tableBorderColor: "rgba(0, 0, 0, 0.12)",
|
||||
|
||||
// General
|
||||
dividerColor: "rgba(0, 0, 0, 0.12)",
|
||||
borderColor: "rgba(0, 0, 0, 0.12)",
|
||||
cardBorderColor: "rgba(0, 0, 0, 0.12)",
|
||||
|
||||
// Icons
|
||||
iconStyle: "filled",
|
||||
} as QThemeMetaData;
|
||||
|
||||
/*******************************************************************************
|
||||
** CSS variable prefix for QQQ theme variables.
|
||||
*******************************************************************************/
|
||||
const CSS_VAR_PREFIX = "--qqq-";
|
||||
|
||||
/*******************************************************************************
|
||||
** Convert a camelCase property name to kebab-case.
|
||||
** Handles both lowercase-to-uppercase and digit-to-uppercase transitions.
|
||||
*******************************************************************************/
|
||||
function toKebabCase(str: string): string
|
||||
{
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
||||
.replace(/([0-9])([A-Z])/g, "$1-$2")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Density spacing multipliers for compact/normal/comfortable modes.
|
||||
*******************************************************************************/
|
||||
const DENSITY_SPACING: Record<string, { base: number; small: string; medium: string; large: string }> = {
|
||||
compact: {base: 6, small: "0.25rem", medium: "0.5rem", large: "0.75rem"},
|
||||
normal: {base: 8, small: "0.5rem", medium: "1rem", large: "1.5rem"},
|
||||
comfortable: {base: 10, small: "0.75rem", medium: "1.25rem", large: "2rem"},
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Convert a hex color to RGB components.
|
||||
*******************************************************************************/
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } | null
|
||||
{
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Lighten a hex color by a percentage (0-100).
|
||||
*******************************************************************************/
|
||||
function lightenColor(hex: string, percent: number): string
|
||||
{
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return hex;
|
||||
|
||||
const factor = percent / 100;
|
||||
const r = Math.min(255, Math.round(rgb.r + (255 - rgb.r) * factor));
|
||||
const g = Math.min(255, Math.round(rgb.g + (255 - rgb.g) * factor));
|
||||
const b = Math.min(255, Math.round(rgb.b + (255 - rgb.b) * factor));
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Darken a hex color by a percentage (0-100).
|
||||
*******************************************************************************/
|
||||
function darkenColor(hex: string, percent: number): string
|
||||
{
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb) return hex;
|
||||
|
||||
const factor = 1 - (percent / 100);
|
||||
const r = Math.round(rgb.r * factor);
|
||||
const g = Math.round(rgb.g * factor);
|
||||
const b = Math.round(rgb.b * factor);
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Inject theme values as CSS custom properties on the :root element.
|
||||
** Merges provided theme with defaults - only defined values override.
|
||||
*******************************************************************************/
|
||||
export function injectThemeVariables(theme?: QThemeMetaData): void
|
||||
{
|
||||
const root = document.documentElement;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Filter out undefined values from theme before merging with defaults //
|
||||
// This prevents undefined values from overwriting default values //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
const definedThemeValues: Partial<QThemeMetaData> = {};
|
||||
if (theme)
|
||||
{
|
||||
for (const key of Object.keys(theme) as (keyof QThemeMetaData)[])
|
||||
{
|
||||
if (theme[key] !== undefined)
|
||||
{
|
||||
(definedThemeValues as Record<string, unknown>)[key] = theme[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
const mergedTheme = {...DEFAULT_THEME, ...definedThemeValues};
|
||||
|
||||
const themeProperties: (keyof QThemeMetaData)[] = [
|
||||
// Color palette
|
||||
"primaryColor",
|
||||
"secondaryColor",
|
||||
"backgroundColor",
|
||||
"surfaceColor",
|
||||
"textPrimary",
|
||||
"textSecondary",
|
||||
"errorColor",
|
||||
"warningColor",
|
||||
"successColor",
|
||||
"infoColor",
|
||||
|
||||
// Typography - Base
|
||||
"fontFamily",
|
||||
"headerFontFamily",
|
||||
"monoFontFamily",
|
||||
"fontSizeBase",
|
||||
"fontWeightLight",
|
||||
"fontWeightRegular",
|
||||
"fontWeightMedium",
|
||||
"fontWeightBold",
|
||||
|
||||
// Typography - H1
|
||||
"typographyH1FontSize",
|
||||
"typographyH1FontWeight",
|
||||
"typographyH1LineHeight",
|
||||
"typographyH1LetterSpacing",
|
||||
"typographyH1TextTransform",
|
||||
|
||||
// Typography - H2
|
||||
"typographyH2FontSize",
|
||||
"typographyH2FontWeight",
|
||||
"typographyH2LineHeight",
|
||||
"typographyH2LetterSpacing",
|
||||
"typographyH2TextTransform",
|
||||
|
||||
// Typography - H3
|
||||
"typographyH3FontSize",
|
||||
"typographyH3FontWeight",
|
||||
"typographyH3LineHeight",
|
||||
"typographyH3LetterSpacing",
|
||||
"typographyH3TextTransform",
|
||||
|
||||
// Typography - H4
|
||||
"typographyH4FontSize",
|
||||
"typographyH4FontWeight",
|
||||
"typographyH4LineHeight",
|
||||
"typographyH4LetterSpacing",
|
||||
"typographyH4TextTransform",
|
||||
|
||||
// Typography - H5
|
||||
"typographyH5FontSize",
|
||||
"typographyH5FontWeight",
|
||||
"typographyH5LineHeight",
|
||||
"typographyH5LetterSpacing",
|
||||
"typographyH5TextTransform",
|
||||
|
||||
// Typography - H6
|
||||
"typographyH6FontSize",
|
||||
"typographyH6FontWeight",
|
||||
"typographyH6LineHeight",
|
||||
"typographyH6LetterSpacing",
|
||||
"typographyH6TextTransform",
|
||||
|
||||
// Typography - Body1
|
||||
"typographyBody1FontSize",
|
||||
"typographyBody1FontWeight",
|
||||
"typographyBody1LineHeight",
|
||||
"typographyBody1LetterSpacing",
|
||||
"typographyBody1TextTransform",
|
||||
|
||||
// Typography - Body2
|
||||
"typographyBody2FontSize",
|
||||
"typographyBody2FontWeight",
|
||||
"typographyBody2LineHeight",
|
||||
"typographyBody2LetterSpacing",
|
||||
"typographyBody2TextTransform",
|
||||
|
||||
// Typography - Button
|
||||
"typographyButtonFontSize",
|
||||
"typographyButtonFontWeight",
|
||||
"typographyButtonLineHeight",
|
||||
"typographyButtonLetterSpacing",
|
||||
"typographyButtonTextTransform",
|
||||
|
||||
// Typography - Caption
|
||||
"typographyCaptionFontSize",
|
||||
"typographyCaptionFontWeight",
|
||||
"typographyCaptionLineHeight",
|
||||
"typographyCaptionLetterSpacing",
|
||||
"typographyCaptionTextTransform",
|
||||
|
||||
// Sizing
|
||||
"borderRadius",
|
||||
"density",
|
||||
|
||||
// Asset paths
|
||||
"logoPath",
|
||||
"iconPath",
|
||||
"faviconPath",
|
||||
|
||||
// Icon style
|
||||
"iconStyle",
|
||||
|
||||
// Branded Header Bar
|
||||
"brandedHeaderEnabled",
|
||||
"brandedHeaderBackgroundColor",
|
||||
"brandedHeaderTextColor",
|
||||
"brandedHeaderLogoPath",
|
||||
"brandedHeaderLogoAltText",
|
||||
"brandedHeaderHeight",
|
||||
"brandedHeaderTagline",
|
||||
|
||||
// App Bar
|
||||
"appBarBackgroundColor",
|
||||
"appBarTextColor",
|
||||
|
||||
// Sidebar
|
||||
"sidebarBackgroundColor",
|
||||
"sidebarTextColor",
|
||||
"sidebarIconColor",
|
||||
"sidebarSelectedBackgroundColor",
|
||||
"sidebarSelectedTextColor",
|
||||
"sidebarHoverBackgroundColor",
|
||||
"sidebarDividerColor",
|
||||
|
||||
// Tables
|
||||
"tableHeaderBackgroundColor",
|
||||
"tableHeaderTextColor",
|
||||
"tableRowHoverColor",
|
||||
"tableRowSelectedColor",
|
||||
"tableBorderColor",
|
||||
|
||||
// General
|
||||
"dividerColor",
|
||||
"borderColor",
|
||||
"cardBorderColor",
|
||||
];
|
||||
|
||||
for (const prop of themeProperties)
|
||||
{
|
||||
const value = mergedTheme[prop];
|
||||
if (value != null)
|
||||
{
|
||||
const cssVarName = `${CSS_VAR_PREFIX}${toKebabCase(prop)}`;
|
||||
root.style.setProperty(cssVarName, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject density-based spacing variables based on density setting //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
const density = mergedTheme.density || "normal";
|
||||
const spacing = DENSITY_SPACING[density] || DENSITY_SPACING.normal;
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}spacing-base`, `${spacing.base}px`);
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}spacing-small`, spacing.small);
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}spacing-medium`, spacing.medium);
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}spacing-large`, spacing.large);
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject derived colors for UI elements //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Action colors for interactive elements
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}action-active`, "rgba(0, 0, 0, 0.54)");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}action-hover`, "rgba(0, 0, 0, 0.04)");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}action-selected`, "rgba(0, 0, 0, 0.08)");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}action-disabled`, "rgba(0, 0, 0, 0.26)");
|
||||
|
||||
// Primary contrast text (text on primary-colored backgrounds)
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}primary-contrast-text`, "#ffffff");
|
||||
|
||||
// Status color variants (light backgrounds and dark text for alerts/banners)
|
||||
const infoColor = mergedTheme.infoColor || DEFAULT_THEME.infoColor;
|
||||
const successColor = mergedTheme.successColor || DEFAULT_THEME.successColor;
|
||||
const warningColor = mergedTheme.warningColor || DEFAULT_THEME.warningColor;
|
||||
const errorColor = mergedTheme.errorColor || DEFAULT_THEME.errorColor;
|
||||
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}info-light`, lightenColor(infoColor, 90));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}info-dark`, darkenColor(infoColor, 60));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}success-light`, lightenColor(successColor, 90));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}success-dark`, darkenColor(successColor, 60));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}warning-light`, lightenColor(warningColor, 90));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}warning-dark`, darkenColor(warningColor, 60));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}error-light`, lightenColor(errorColor, 90));
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}error-dark`, darkenColor(errorColor, 60));
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject chart-specific colors //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}chart-grid-color`, "#c1c4ce5c");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}chart-text-color`, "#9ca2b7");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}chart-background-color`, mergedTheme.primaryColor || "#1A73E8");
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject link color (defaults to primary) //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}link-color`, mergedTheme.primaryColor || DEFAULT_THEME.primaryColor);
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject grey scale for UI elements //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-100`, "#f8f9fa");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-200`, "#eeeeee");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-300`, "#e0e0e0");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-400`, "#bdbdbd");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-500`, "#9e9e9e");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-600`, "#757575");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-700`, "#616161");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-800`, "#424242");
|
||||
root.style.setProperty(`${CSS_VAR_PREFIX}grey-900`, "#212121");
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject icon font stylesheets dynamically //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
injectIconFonts(mergedTheme.iconStyle);
|
||||
|
||||
// Inject custom CSS if provided
|
||||
if (mergedTheme.customCss)
|
||||
{
|
||||
injectCustomCss(mergedTheme.customCss);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Map icon style name to MUI Icon baseClassName.
|
||||
*******************************************************************************/
|
||||
const ICON_STYLE_TO_CLASS: Record<string, string> = {
|
||||
filled: "material-icons",
|
||||
outlined: "material-icons-outlined",
|
||||
rounded: "material-icons-round",
|
||||
sharp: "material-icons-sharp",
|
||||
"two-tone": "material-icons-two-tone",
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the MUI Icon baseClassName for the current theme's iconStyle.
|
||||
*******************************************************************************/
|
||||
export function getIconBaseClassName(): string
|
||||
{
|
||||
const iconStyle = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--qqq-icon-style").trim() || "filled";
|
||||
return ICON_STYLE_TO_CLASS[iconStyle] || ICON_STYLE_TO_CLASS.filled;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Retrieve a CSS variable value with an optional fallback.
|
||||
*******************************************************************************/
|
||||
export function getThemeVariable(name: string, fallback?: string): string
|
||||
{
|
||||
const cssVarName = name.startsWith(CSS_VAR_PREFIX) ? name : `${CSS_VAR_PREFIX}${toKebabCase(name)}`;
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(cssVarName).trim();
|
||||
return value || fallback || "";
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Inject custom CSS into the document head.
|
||||
*******************************************************************************/
|
||||
function injectCustomCss(css: string): void
|
||||
{
|
||||
const existingStyle = document.getElementById("qqq-custom-theme-css");
|
||||
if (existingStyle)
|
||||
{
|
||||
existingStyle.textContent = css;
|
||||
}
|
||||
else
|
||||
{
|
||||
const style = document.createElement("style");
|
||||
style.id = "qqq-custom-theme-css";
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Default icon font CDN URL for Material Icons.
|
||||
*******************************************************************************/
|
||||
const DEFAULT_ICON_FONT_URL = "https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Two+Tone|Material+Icons+Round|Material+Icons+Sharp";
|
||||
|
||||
/*******************************************************************************
|
||||
** Default font CDN URL for Roboto.
|
||||
*******************************************************************************/
|
||||
const DEFAULT_FONT_URL = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap";
|
||||
|
||||
/*******************************************************************************
|
||||
** Inject icon font stylesheets dynamically based on theme configuration.
|
||||
** This replaces the hardcoded CDN links in index.html with dynamic injection.
|
||||
*******************************************************************************/
|
||||
function injectIconFonts(iconStyle?: string): void
|
||||
{
|
||||
const iconFontLinkId = "qqq-icon-font";
|
||||
const fontLinkId = "qqq-font";
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject main font (Roboto) if not already present //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
if (!document.getElementById(fontLinkId))
|
||||
{
|
||||
const fontLink = document.createElement("link");
|
||||
fontLink.id = fontLinkId;
|
||||
fontLink.rel = "stylesheet";
|
||||
fontLink.href = DEFAULT_FONT_URL;
|
||||
document.head.appendChild(fontLink);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Inject icon font - remove existing and re-add to allow updates //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
const existingIconLink = document.getElementById(iconFontLinkId);
|
||||
if (existingIconLink)
|
||||
{
|
||||
existingIconLink.remove();
|
||||
}
|
||||
|
||||
const iconLink = document.createElement("link");
|
||||
iconLink.id = iconFontLinkId;
|
||||
iconLink.rel = "stylesheet";
|
||||
iconLink.href = DEFAULT_ICON_FONT_URL;
|
||||
document.head.appendChild(iconLink);
|
||||
}
|
||||
+1397
File diff suppressed because it is too large.
Load diff
+893
@@ -0,0 +1,893 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Selenium E2E tests for pluggable themes feature.
|
||||
** Tests that theme properties from backend metaData are applied as CSS variables.
|
||||
*******************************************************************************/
|
||||
public class ThemeIT extends QBaseSeleniumTest
|
||||
{
|
||||
private String metaDataFixture = "metaData/index.json";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||
{
|
||||
qSeleniumJavalin
|
||||
.withRouteToFile("/metaData", metaDataFixture)
|
||||
.withRouteToFile("/metaData/authentication", "metaData/authentication.json")
|
||||
.withRouteToFile("/metaData/table/person", "metaData/table/person.json")
|
||||
.withRouteToFile("/qqq/v1/metaData/table/person", "qqq/v1/metaData/table/person.json")
|
||||
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Helper to get a CSS variable value from :root.
|
||||
*******************************************************************************/
|
||||
private String getCssVariable(String variableName)
|
||||
{
|
||||
JavascriptExecutor js = (JavascriptExecutor) driver;
|
||||
String script = "return getComputedStyle(document.documentElement).getPropertyValue('" + variableName + "').trim();";
|
||||
return (String) js.executeScript(script);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Helper to check if a style element with specific ID exists.
|
||||
*******************************************************************************/
|
||||
private boolean styleElementExists(String id)
|
||||
{
|
||||
JavascriptExecutor js = (JavascriptExecutor) driver;
|
||||
String script = "return document.getElementById('" + id + "') !== null;";
|
||||
return (Boolean) js.executeScript(script);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Helper to get content of a style element.
|
||||
*******************************************************************************/
|
||||
private String getStyleElementContent(String id)
|
||||
{
|
||||
JavascriptExecutor js = (JavascriptExecutor) driver;
|
||||
String script = "var el = document.getElementById('" + id + "'); return el ? el.textContent : '';";
|
||||
return (String) js.executeScript(script);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom primary color is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testPrimaryColorApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String primaryColor = getCssVariable("--qqq-primary-color");
|
||||
assertThat(primaryColor).isEqualToIgnoringCase("#FF5500");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom secondary color is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSecondaryColorApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String secondaryColor = getCssVariable("--qqq-secondary-color");
|
||||
assertThat(secondaryColor).isEqualToIgnoringCase("#00AA55");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom background color is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testBackgroundColorApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String backgroundColor = getCssVariable("--qqq-background-color");
|
||||
assertThat(backgroundColor).isEqualToIgnoringCase("#EEEEFF");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that all color properties are applied from custom theme.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAllColorPropertiesApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-primary-color")).isEqualToIgnoringCase("#FF5500");
|
||||
assertThat(getCssVariable("--qqq-secondary-color")).isEqualToIgnoringCase("#00AA55");
|
||||
assertThat(getCssVariable("--qqq-background-color")).isEqualToIgnoringCase("#EEEEFF");
|
||||
assertThat(getCssVariable("--qqq-surface-color")).isEqualToIgnoringCase("#FFFFFF");
|
||||
assertThat(getCssVariable("--qqq-text-primary")).isEqualToIgnoringCase("#111111");
|
||||
assertThat(getCssVariable("--qqq-text-secondary")).isEqualToIgnoringCase("#555555");
|
||||
assertThat(getCssVariable("--qqq-error-color")).isEqualToIgnoringCase("#CC0000");
|
||||
assertThat(getCssVariable("--qqq-warning-color")).isEqualToIgnoringCase("#FFAA00");
|
||||
assertThat(getCssVariable("--qqq-success-color")).isEqualToIgnoringCase("#00CC00");
|
||||
assertThat(getCssVariable("--qqq-info-color")).isEqualToIgnoringCase("#0055FF");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom font family is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFontFamilyApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String fontFamily = getCssVariable("--qqq-font-family");
|
||||
assertThat(fontFamily).contains("Arial");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom header font family is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testHeaderFontFamilyApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String headerFontFamily = getCssVariable("--qqq-header-font-family");
|
||||
assertThat(headerFontFamily).contains("Georgia");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom border radius is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testBorderRadiusApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String borderRadius = getCssVariable("--qqq-border-radius");
|
||||
assertThat(borderRadius).isEqualTo("12px");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that compact density sets appropriate spacing variables.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCompactDensitySpacing()
|
||||
{
|
||||
metaDataFixture = "metaData/withCompactDensity.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-density")).isEqualTo("compact");
|
||||
assertThat(getCssVariable("--qqq-spacing-base")).isEqualTo("6px");
|
||||
assertThat(getCssVariable("--qqq-spacing-small")).isEqualTo("0.25rem");
|
||||
assertThat(getCssVariable("--qqq-spacing-medium")).isEqualTo("0.5rem");
|
||||
assertThat(getCssVariable("--qqq-spacing-large")).isEqualTo("0.75rem");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that outlined icon style is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOutlinedIconStyle()
|
||||
{
|
||||
metaDataFixture = "metaData/withOutlinedIcons.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String iconStyle = getCssVariable("--qqq-icon-style");
|
||||
assertThat(iconStyle).isEqualTo("outlined");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that custom CSS is injected into the page.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCustomCssInjected()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomCss.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Verify the custom CSS style element was created //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
assertThat(styleElementExists("qqq-custom-theme-css")).isTrue();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Verify the content matches what was provided //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
String cssContent = getStyleElementContent("qqq-custom-theme-css");
|
||||
assertThat(cssContent).contains(".qqq-test-marker");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that default theme values are used when no theme is provided.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDefaultThemeWhenNotProvided()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Use the standard index.json which has no theme property //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
metaDataFixture = "metaData/index.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Greetings App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Default primary color should be QQQ blue //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
String primaryColor = getCssVariable("--qqq-primary-color");
|
||||
assertThat(primaryColor).isEqualToIgnoringCase("#0062FF");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that normal density (default) sets appropriate spacing variables.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNormalDensitySpacing()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-density")).isEqualTo("normal");
|
||||
assertThat(getCssVariable("--qqq-spacing-base")).isEqualTo("8px");
|
||||
assertThat(getCssVariable("--qqq-spacing-small")).isEqualTo("0.5rem");
|
||||
assertThat(getCssVariable("--qqq-spacing-medium")).isEqualTo("1rem");
|
||||
assertThat(getCssVariable("--qqq-spacing-large")).isEqualTo("1.5rem");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that filled icon style is applied by default.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFilledIconStyleDefault()
|
||||
{
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
String iconStyle = getCssVariable("--qqq-icon-style");
|
||||
assertThat(iconStyle).isEqualTo("filled");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that branded header CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testBrandedHeaderCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-branded-header-enabled")).isEqualTo("true");
|
||||
assertThat(getCssVariable("--qqq-branded-header-background-color")).isEqualToIgnoringCase("#1A237E");
|
||||
assertThat(getCssVariable("--qqq-branded-header-text-color")).isEqualToIgnoringCase("#E8EAF6");
|
||||
assertThat(getCssVariable("--qqq-branded-header-logo-path")).isEqualTo("/branded-header-logo.png");
|
||||
assertThat(getCssVariable("--qqq-branded-header-logo-alt-text")).isEqualTo("Test Brand Logo");
|
||||
assertThat(getCssVariable("--qqq-branded-header-height")).isEqualTo("56px");
|
||||
assertThat(getCssVariable("--qqq-branded-header-tagline")).isEqualTo("Custom Theme Test Suite");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that sidebar CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSidebarCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-sidebar-background-color")).isEqualToIgnoringCase("#1B5E20");
|
||||
assertThat(getCssVariable("--qqq-sidebar-text-color")).isEqualToIgnoringCase("#C8E6C9");
|
||||
assertThat(getCssVariable("--qqq-sidebar-icon-color")).isEqualToIgnoringCase("#A5D6A7");
|
||||
assertThat(getCssVariable("--qqq-sidebar-selected-background-color")).isEqualToIgnoringCase("#2E7D32");
|
||||
assertThat(getCssVariable("--qqq-sidebar-selected-text-color")).isEqualToIgnoringCase("#FFFFFF");
|
||||
assertThat(getCssVariable("--qqq-sidebar-hover-background-color")).contains("rgba");
|
||||
assertThat(getCssVariable("--qqq-sidebar-divider-color")).contains("rgba");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that app bar CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAppBarCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-app-bar-background-color")).isEqualToIgnoringCase("#F5F5F5");
|
||||
assertThat(getCssVariable("--qqq-app-bar-text-color")).isEqualToIgnoringCase("#37474F");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that table CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTableCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-table-header-background-color")).isEqualToIgnoringCase("#7B1FA2");
|
||||
assertThat(getCssVariable("--qqq-table-header-text-color")).isEqualToIgnoringCase("#FFFFFF");
|
||||
assertThat(getCssVariable("--qqq-table-row-hover-color")).isEqualToIgnoringCase("#F3E5F5");
|
||||
assertThat(getCssVariable("--qqq-table-row-selected-color")).isEqualToIgnoringCase("#CE93D8");
|
||||
assertThat(getCssVariable("--qqq-table-border-color")).isEqualToIgnoringCase("#E1BEE7");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that divider and border CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDividerBorderCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-divider-color")).isEqualToIgnoringCase("#CFD8DC");
|
||||
assertThat(getCssVariable("--qqq-border-color")).isEqualToIgnoringCase("#B0BEC5");
|
||||
assertThat(getCssVariable("--qqq-card-border-color")).isEqualToIgnoringCase("#ECEFF1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that typography H1-H3 CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTypographyH1H3CssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// H1 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-h1-font-size")).isEqualTo("3rem");
|
||||
assertThat(getCssVariable("--qqq-typography-h1-font-weight")).isEqualTo("700");
|
||||
assertThat(getCssVariable("--qqq-typography-h1-line-height")).isEqualTo("1.2");
|
||||
assertThat(getCssVariable("--qqq-typography-h1-letter-spacing")).isEqualTo("-0.02em");
|
||||
assertThat(getCssVariable("--qqq-typography-h1-text-transform")).isEqualTo("none");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// H2 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-h2-font-size")).isEqualTo("2.5rem");
|
||||
assertThat(getCssVariable("--qqq-typography-h2-font-weight")).isEqualTo("600");
|
||||
assertThat(getCssVariable("--qqq-typography-h2-line-height")).isEqualTo("1.3");
|
||||
assertThat(getCssVariable("--qqq-typography-h2-letter-spacing")).isEqualTo("-0.01em");
|
||||
assertThat(getCssVariable("--qqq-typography-h2-text-transform")).isEqualTo("none");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// H3 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-h3-font-size")).isEqualTo("2rem");
|
||||
assertThat(getCssVariable("--qqq-typography-h3-font-weight")).isEqualTo("600");
|
||||
assertThat(getCssVariable("--qqq-typography-h3-line-height")).isEqualTo("1.4");
|
||||
assertThat(getCssVariable("--qqq-typography-h3-letter-spacing")).isEqualTo("0");
|
||||
assertThat(getCssVariable("--qqq-typography-h3-text-transform")).isEqualTo("none");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that typography H4-H6 CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTypographyH4H6CssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// H4 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-h4-font-size")).isEqualTo("1.5rem");
|
||||
assertThat(getCssVariable("--qqq-typography-h4-font-weight")).isEqualTo("500");
|
||||
assertThat(getCssVariable("--qqq-typography-h4-line-height")).isEqualTo("1.4");
|
||||
assertThat(getCssVariable("--qqq-typography-h4-letter-spacing")).isEqualTo("0");
|
||||
assertThat(getCssVariable("--qqq-typography-h4-text-transform")).isEqualTo("none");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// H5 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-h5-font-size")).isEqualTo("1.25rem");
|
||||
assertThat(getCssVariable("--qqq-typography-h5-font-weight")).isEqualTo("500");
|
||||
assertThat(getCssVariable("--qqq-typography-h5-line-height")).isEqualTo("1.5");
|
||||
assertThat(getCssVariable("--qqq-typography-h5-letter-spacing")).isEqualTo("0");
|
||||
assertThat(getCssVariable("--qqq-typography-h5-text-transform")).isEqualTo("none");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// H6 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-h6-font-size")).isEqualTo("1rem");
|
||||
assertThat(getCssVariable("--qqq-typography-h6-font-weight")).isEqualTo("500");
|
||||
assertThat(getCssVariable("--qqq-typography-h6-line-height")).isEqualTo("1.5");
|
||||
assertThat(getCssVariable("--qqq-typography-h6-letter-spacing")).isEqualTo("0.01em");
|
||||
assertThat(getCssVariable("--qqq-typography-h6-text-transform")).isEqualTo("none");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that typography body and caption CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTypographyBodyCaptionCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Body1 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-body1-font-size")).isEqualTo("1rem");
|
||||
assertThat(getCssVariable("--qqq-typography-body1-font-weight")).isEqualTo("400");
|
||||
assertThat(getCssVariable("--qqq-typography-body1-line-height")).isEqualTo("1.6");
|
||||
assertThat(getCssVariable("--qqq-typography-body1-letter-spacing")).isEqualTo("0");
|
||||
assertThat(getCssVariable("--qqq-typography-body1-text-transform")).isEqualTo("none");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Body2 typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-body2-font-size")).isEqualTo("0.875rem");
|
||||
assertThat(getCssVariable("--qqq-typography-body2-font-weight")).isEqualTo("400");
|
||||
assertThat(getCssVariable("--qqq-typography-body2-line-height")).isEqualTo("1.5");
|
||||
assertThat(getCssVariable("--qqq-typography-body2-letter-spacing")).isEqualTo("0");
|
||||
assertThat(getCssVariable("--qqq-typography-body2-text-transform")).isEqualTo("none");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Caption typography //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-typography-caption-font-size")).isEqualTo("0.75rem");
|
||||
assertThat(getCssVariable("--qqq-typography-caption-font-weight")).isEqualTo("400");
|
||||
assertThat(getCssVariable("--qqq-typography-caption-line-height")).isEqualTo("1.4");
|
||||
assertThat(getCssVariable("--qqq-typography-caption-letter-spacing")).isEqualTo("0.03em");
|
||||
assertThat(getCssVariable("--qqq-typography-caption-text-transform")).isEqualTo("none");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that typography button CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTypographyButtonCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-typography-button-font-size")).isEqualTo("0.875rem");
|
||||
assertThat(getCssVariable("--qqq-typography-button-font-weight")).isEqualTo("600");
|
||||
assertThat(getCssVariable("--qqq-typography-button-line-height")).isEqualTo("1.75");
|
||||
assertThat(getCssVariable("--qqq-typography-button-letter-spacing")).isEqualTo("0.03em");
|
||||
assertThat(getCssVariable("--qqq-typography-button-text-transform")).isEqualTo("uppercase");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that font weight CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFontWeightCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-font-weight-light")).isEqualTo("300");
|
||||
assertThat(getCssVariable("--qqq-font-weight-regular")).isEqualTo("400");
|
||||
assertThat(getCssVariable("--qqq-font-weight-medium")).isEqualTo("500");
|
||||
assertThat(getCssVariable("--qqq-font-weight-bold")).isEqualTo("700");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that monospace font family CSS variable is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testMonoFontFamilyApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
String monoFontFamily = getCssVariable("--qqq-mono-font-family");
|
||||
assertThat(monoFontFamily).contains("JetBrains Mono");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that font size base CSS variable is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFontSizeBaseApplied()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-font-size-base")).isEqualTo("15px");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that comfortable density sets appropriate spacing variables.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testComfortableDensitySpacing()
|
||||
{
|
||||
metaDataFixture = "metaData/withComfortableDensity.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Comfortable Density Test");
|
||||
|
||||
assertThat(getCssVariable("--qqq-density")).isEqualTo("comfortable");
|
||||
assertThat(getCssVariable("--qqq-spacing-base")).isEqualTo("10px");
|
||||
assertThat(getCssVariable("--qqq-spacing-small")).isEqualTo("0.75rem");
|
||||
assertThat(getCssVariable("--qqq-spacing-medium")).isEqualTo("1.25rem");
|
||||
assertThat(getCssVariable("--qqq-spacing-large")).isEqualTo("2rem");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that rounded icon style is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRoundedIconStyle()
|
||||
{
|
||||
metaDataFixture = "metaData/withRoundedIcons.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Rounded Icons Test");
|
||||
|
||||
String iconStyle = getCssVariable("--qqq-icon-style");
|
||||
assertThat(iconStyle).isEqualTo("rounded");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that sharp icon style is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSharpIconStyle()
|
||||
{
|
||||
metaDataFixture = "metaData/withSharpIcons.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Sharp Icons Test");
|
||||
|
||||
String iconStyle = getCssVariable("--qqq-icon-style");
|
||||
assertThat(iconStyle).isEqualTo("sharp");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that two-tone icon style is applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoToneIconStyle()
|
||||
{
|
||||
metaDataFixture = "metaData/withTwoToneIcons.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Two Tone Icons Test");
|
||||
|
||||
String iconStyle = getCssVariable("--qqq-icon-style");
|
||||
assertThat(iconStyle).isEqualTo("two-tone");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that logo/icon/favicon path CSS variables are applied.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testBrandingPathCssVariables()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
assertThat(getCssVariable("--qqq-logo-path")).isEqualTo("/custom-logo.png");
|
||||
assertThat(getCssVariable("--qqq-icon-path")).isEqualTo("/custom-icon.png");
|
||||
assertThat(getCssVariable("--qqq-favicon-path")).isEqualTo("/custom-favicon.ico");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test comprehensive theme with all properties applied together.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAllThemePropertiesAppliedTogether()
|
||||
{
|
||||
metaDataFixture = "metaData/withFullCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Full Theme Test App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Core colors //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-primary-color")).isEqualToIgnoringCase("#E91E63");
|
||||
assertThat(getCssVariable("--qqq-secondary-color")).isEqualToIgnoringCase("#9C27B0");
|
||||
assertThat(getCssVariable("--qqq-background-color")).isEqualToIgnoringCase("#ECEFF1");
|
||||
assertThat(getCssVariable("--qqq-surface-color")).isEqualToIgnoringCase("#FAFAFA");
|
||||
assertThat(getCssVariable("--qqq-text-primary")).isEqualToIgnoringCase("#263238");
|
||||
assertThat(getCssVariable("--qqq-text-secondary")).isEqualToIgnoringCase("#607D8B");
|
||||
assertThat(getCssVariable("--qqq-error-color")).isEqualToIgnoringCase("#D50000");
|
||||
assertThat(getCssVariable("--qqq-warning-color")).isEqualToIgnoringCase("#FF6D00");
|
||||
assertThat(getCssVariable("--qqq-success-color")).isEqualToIgnoringCase("#00C853");
|
||||
assertThat(getCssVariable("--qqq-info-color")).isEqualToIgnoringCase("#2962FF");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Font families //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-font-family")).contains("Inter");
|
||||
assertThat(getCssVariable("--qqq-header-font-family")).contains("Poppins");
|
||||
assertThat(getCssVariable("--qqq-mono-font-family")).contains("JetBrains Mono");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Border radius and density //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-border-radius")).isEqualTo("12px");
|
||||
assertThat(getCssVariable("--qqq-density")).isEqualTo("normal");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Icon style //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-icon-style")).isEqualTo("rounded");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Verify custom CSS was injected //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(styleElementExists("qqq-custom-theme-css")).isTrue();
|
||||
String cssContent = getStyleElementContent("qqq-custom-theme-css");
|
||||
assertThat(cssContent).contains(".qqq-full-theme-test-marker");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that partial theme configuration only sets specified properties.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testPartialThemeConfiguration()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Use the standard custom theme which only has a subset of properties //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
metaDataFixture = "metaData/withCustomTheme.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.restart();
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Test App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// These were explicitly set in the fixture //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-primary-color")).isEqualToIgnoringCase("#FF5500");
|
||||
assertThat(getCssVariable("--qqq-secondary-color")).isEqualToIgnoringCase("#00AA55");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Verify defaults are used for unset sidebar properties //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
String sidebarBg = getCssVariable("--qqq-sidebar-background-color");
|
||||
assertThat(sidebarBg).isNotEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that empty theme object uses all defaults.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEmptyThemeUsesDefaults()
|
||||
{
|
||||
metaDataFixture = "metaData/index.json";
|
||||
qSeleniumJavalin.clearRoutes();
|
||||
addJavalinRoutes(qSeleniumJavalin);
|
||||
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/", "Greetings App");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Verify default values are applied //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
assertThat(getCssVariable("--qqq-primary-color")).isEqualToIgnoringCase("#0062FF");
|
||||
assertThat(getCssVariable("--qqq-density")).isEqualTo("normal");
|
||||
assertThat(getCssVariable("--qqq-icon-style")).isEqualTo("filled");
|
||||
assertThat(getCssVariable("--qqq-border-radius")).isEqualTo("8px");
|
||||
}
|
||||
}
|
||||
+1406
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Branded Header Test App",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Branded Header Test App", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Branded Header Test App",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"brandedHeaderEnabled": true,
|
||||
"brandedHeaderBackgroundColor": "#0D47A1",
|
||||
"brandedHeaderTextColor": "#FFFFFF",
|
||||
"brandedHeaderLogoPath": "/branded-logo.png",
|
||||
"brandedHeaderLogoAltText": "Brand Logo",
|
||||
"brandedHeaderHeight": "52px",
|
||||
"brandedHeaderTagline": "Enterprise Platform"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Comfortable Density Test",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Comfortable Density Test", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Comfortable Density Test",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"density": "comfortable"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Test App", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"density": "compact"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Test App", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"customCss": ".qqq-test-marker { display: block !important; }"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Test App", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"primaryColor": "#FF5500",
|
||||
"secondaryColor": "#00AA55",
|
||||
"backgroundColor": "#EEEEFF",
|
||||
"surfaceColor": "#FFFFFF",
|
||||
"textPrimary": "#111111",
|
||||
"textSecondary": "#555555",
|
||||
"errorColor": "#CC0000",
|
||||
"warningColor": "#FFAA00",
|
||||
"successColor": "#00CC00",
|
||||
"infoColor": "#0055FF",
|
||||
"fontFamily": "\"Arial\", sans-serif",
|
||||
"headerFontFamily": "\"Georgia\", serif",
|
||||
"borderRadius": "12px",
|
||||
"density": "normal",
|
||||
"iconStyle": "filled"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"],
|
||||
"primaryKeyField": "id",
|
||||
"fields": {
|
||||
"id": {"name": "id", "label": "Id", "type": "INTEGER", "isEditable": false},
|
||||
"firstName": {"name": "firstName", "label": "First Name", "type": "STRING", "isEditable": true},
|
||||
"lastName": {"name": "lastName", "label": "Last Name", "type": "STRING", "isEditable": true}
|
||||
}
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Full Theme Test App",
|
||||
"iconName": "palette",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Full Theme Test App", "icon": {"name": "palette"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Full Theme Test App",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "palette"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/test-logo.png",
|
||||
"icon": "/test-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"primaryColor": "#E91E63",
|
||||
"secondaryColor": "#9C27B0",
|
||||
"backgroundColor": "#ECEFF1",
|
||||
"surfaceColor": "#FAFAFA",
|
||||
"textPrimary": "#263238",
|
||||
"textSecondary": "#607D8B",
|
||||
"errorColor": "#D50000",
|
||||
"warningColor": "#FF6D00",
|
||||
"successColor": "#00C853",
|
||||
"infoColor": "#2962FF",
|
||||
|
||||
"fontFamily": "\"Inter\", \"Helvetica\", sans-serif",
|
||||
"headerFontFamily": "\"Poppins\", \"Helvetica\", sans-serif",
|
||||
"monoFontFamily": "\"JetBrains Mono\", monospace",
|
||||
"fontSizeBase": "15px",
|
||||
"fontWeightLight": 300,
|
||||
"fontWeightRegular": 400,
|
||||
"fontWeightMedium": 500,
|
||||
"fontWeightBold": 700,
|
||||
|
||||
"typographyH1FontSize": "3rem",
|
||||
"typographyH1FontWeight": 700,
|
||||
"typographyH1LineHeight": "1.2",
|
||||
"typographyH1LetterSpacing": "-0.02em",
|
||||
"typographyH1TextTransform": "none",
|
||||
|
||||
"typographyH2FontSize": "2.5rem",
|
||||
"typographyH2FontWeight": 600,
|
||||
"typographyH2LineHeight": "1.3",
|
||||
"typographyH2LetterSpacing": "-0.01em",
|
||||
"typographyH2TextTransform": "none",
|
||||
|
||||
"typographyH3FontSize": "2rem",
|
||||
"typographyH3FontWeight": 600,
|
||||
"typographyH3LineHeight": "1.4",
|
||||
"typographyH3LetterSpacing": "0",
|
||||
"typographyH3TextTransform": "none",
|
||||
|
||||
"typographyH4FontSize": "1.5rem",
|
||||
"typographyH4FontWeight": 500,
|
||||
"typographyH4LineHeight": "1.4",
|
||||
"typographyH4LetterSpacing": "0",
|
||||
"typographyH4TextTransform": "none",
|
||||
|
||||
"typographyH5FontSize": "1.25rem",
|
||||
"typographyH5FontWeight": 500,
|
||||
"typographyH5LineHeight": "1.5",
|
||||
"typographyH5LetterSpacing": "0",
|
||||
"typographyH5TextTransform": "none",
|
||||
|
||||
"typographyH6FontSize": "1rem",
|
||||
"typographyH6FontWeight": 500,
|
||||
"typographyH6LineHeight": "1.5",
|
||||
"typographyH6LetterSpacing": "0.01em",
|
||||
"typographyH6TextTransform": "none",
|
||||
|
||||
"typographyBody1FontSize": "1rem",
|
||||
"typographyBody1FontWeight": 400,
|
||||
"typographyBody1LineHeight": "1.6",
|
||||
"typographyBody1LetterSpacing": "0",
|
||||
"typographyBody1TextTransform": "none",
|
||||
|
||||
"typographyBody2FontSize": "0.875rem",
|
||||
"typographyBody2FontWeight": 400,
|
||||
"typographyBody2LineHeight": "1.5",
|
||||
"typographyBody2LetterSpacing": "0",
|
||||
"typographyBody2TextTransform": "none",
|
||||
|
||||
"typographyButtonFontSize": "0.875rem",
|
||||
"typographyButtonFontWeight": 600,
|
||||
"typographyButtonLineHeight": "1.75",
|
||||
"typographyButtonLetterSpacing": "0.03em",
|
||||
"typographyButtonTextTransform": "uppercase",
|
||||
|
||||
"typographyCaptionFontSize": "0.75rem",
|
||||
"typographyCaptionFontWeight": 400,
|
||||
"typographyCaptionLineHeight": "1.4",
|
||||
"typographyCaptionLetterSpacing": "0.03em",
|
||||
"typographyCaptionTextTransform": "none",
|
||||
|
||||
"borderRadius": "12px",
|
||||
"density": "normal",
|
||||
|
||||
"logoPath": "/custom-logo.png",
|
||||
"iconPath": "/custom-icon.png",
|
||||
"faviconPath": "/custom-favicon.ico",
|
||||
|
||||
"iconStyle": "rounded",
|
||||
|
||||
"brandedHeaderEnabled": true,
|
||||
"brandedHeaderBackgroundColor": "#1A237E",
|
||||
"brandedHeaderTextColor": "#E8EAF6",
|
||||
"brandedHeaderLogoPath": "/branded-header-logo.png",
|
||||
"brandedHeaderLogoAltText": "Test Brand Logo",
|
||||
"brandedHeaderHeight": "56px",
|
||||
"brandedHeaderTagline": "Custom Theme Test Suite",
|
||||
|
||||
"appBarBackgroundColor": "#F5F5F5",
|
||||
"appBarTextColor": "#37474F",
|
||||
|
||||
"sidebarBackgroundColor": "#1B5E20",
|
||||
"sidebarTextColor": "#C8E6C9",
|
||||
"sidebarIconColor": "#A5D6A7",
|
||||
"sidebarSelectedBackgroundColor": "#2E7D32",
|
||||
"sidebarSelectedTextColor": "#FFFFFF",
|
||||
"sidebarHoverBackgroundColor": "rgba(255, 255, 255, 0.15)",
|
||||
"sidebarDividerColor": "rgba(255, 255, 255, 0.25)",
|
||||
|
||||
"tableHeaderBackgroundColor": "#7B1FA2",
|
||||
"tableHeaderTextColor": "#FFFFFF",
|
||||
"tableRowHoverColor": "#F3E5F5",
|
||||
"tableRowSelectedColor": "#CE93D8",
|
||||
"tableBorderColor": "#E1BEE7",
|
||||
|
||||
"dividerColor": "#CFD8DC",
|
||||
"borderColor": "#B0BEC5",
|
||||
"cardBorderColor": "#ECEFF1",
|
||||
|
||||
"customCss": ".qqq-full-theme-test-marker { display: block; }"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Test App", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Test App",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"iconStyle": "outlined"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Rounded Icons Test",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Rounded Icons Test", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Rounded Icons Test",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"iconStyle": "rounded"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Sharp Icons Test",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Sharp Icons Test", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Sharp Icons Test",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"iconStyle": "sharp"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"tables": {
|
||||
"person": {
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"capabilities": ["TABLE_COUNT", "TABLE_GET", "TABLE_QUERY", "TABLE_UPDATE", "TABLE_INSERT", "TABLE_DELETE"]
|
||||
}
|
||||
},
|
||||
"processes": {},
|
||||
"apps": {
|
||||
"testApp": {
|
||||
"name": "testApp",
|
||||
"label": "Two Tone Icons Test",
|
||||
"iconName": "settings",
|
||||
"widgets": [],
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"childMap": {
|
||||
"person": {"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
},
|
||||
"sections": [
|
||||
{"name": "testApp", "label": "Two Tone Icons Test", "icon": {"name": "badge"}, "tables": ["person"]}
|
||||
]
|
||||
}
|
||||
},
|
||||
"appTree": [
|
||||
{
|
||||
"type": "APP",
|
||||
"name": "testApp",
|
||||
"label": "Two Tone Icons Test",
|
||||
"children": [
|
||||
{"type": "TABLE", "name": "person", "label": "Person", "iconName": "person"}
|
||||
],
|
||||
"iconName": "settings"
|
||||
}
|
||||
],
|
||||
"branding": {
|
||||
"logo": "/kr-logo.png",
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"supplementalInstanceMetaData": {
|
||||
"theme": {
|
||||
"iconStyle": "two-tone"
|
||||
}
|
||||
},
|
||||
"widgets": {},
|
||||
"environmentValues": {
|
||||
"MATERIAL_UI_LICENSE_KEY": "ABCDE"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user