diff --git a/docs/superpowers/plans/2026-04-14-m3-token-consolidation.md b/docs/superpowers/plans/2026-04-14-m3-token-consolidation.md new file mode 100644 index 0000000000..829a9ec821 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-m3-token-consolidation.md @@ -0,0 +1,1148 @@ +# M3 Token Consolidation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extract every hardcoded color, radius, size, spacing, and elevation value across `eform-angular-frontend` and all 18 plugin source repos into a single token vocabulary, mechanically replace hardcodes with `var(--…)` references, and gate the merge on a Playwright screenshot-regression diff against current `stable`. + +**Architecture:** Three sequential phases — (1) audit + vocabulary, (2) parallel mechanical replace, (3) screenshot diff + fix loop. Phase 1 produces a locked vocabulary that phase 2 subagents are forbidden to extend without going through the orchestrator. Phase 3 enforces visual neutrality. + +**Tech Stack:** Angular Material 20 (M3), SCSS, Playwright (MCP) for capture, `pixelmatch` for diff, `git worktree` per branch. + +**Reference spec:** `docs/superpowers/specs/2026-04-14-m3-token-consolidation-design.md` + +**Plugin source repo list (18 plugins, established 2026-04-14):** +- `eform-angular-appointment-plugin` +- `eform-angular-basecustomer-plugin` +- `eform-angular-chemical-plugin` +- `eform-angular-eform-dashboard-plugin` +- `eform-angular-greate-belt-plugin` +- `eform-angular-insight-dashboard-plugin` +- `eform-angular-inventory-plugin` +- `eform-angular-items-planning-plugin` +- `eform-angular-monitoring-plugin` +- `eform-angular-outer-inner-resource-plugin` +- `eform-angular-rentableitem-plugin` +- `eform-angular-sportfederation-plugin` +- `eform-angular-timeplanning-plugin` +- `eform-angular-trash-inspection-plugin` +- `eform-angular-workflow-plugin` +- `eform-angular-work-orders-plugin` +- `eform-backendconfiguration-plugin` (no `-angular-` prefix) +- `eform-kanban-plugin` (no `-angular-` prefix) + +All paths under `/home/rene/Documents/workspace/microting/`. + +--- + +## Phase 0 — Preserve current state, prepare branches + +### Task 0.1: Commit the current dirty work on `feature/theme-switcher-workspace` + +**Files:** +- All currently-modified files in `eform-angular-frontend` (the in-progress eform parity fixes) + +- [ ] **Step 1: Inspect uncommitted state** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git status +git diff --stat +``` + +Expected: the eform parity SCSS edits we've been iterating on (theme tokens, dropdown, nav, buttons), plus any untracked new files. + +- [ ] **Step 2: Stage and commit** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +# Stage explicitly — never `git add .` +git add eform-client/src/scss/themes/_eform.scss +git add eform-client/src/scss/themes/_theme-mixin.scss +git add eform-client/src/scss/components/_material-dropdown.scss +git add eform-client/src/scss/libs/theme.scss +git add eform-client/src/scss/styles.scss +git add eform-client/src/app/components/navigation/navigation.component.scss +# Add any other modified files surfaced by Step 1 +git status # confirm only intended files staged + +git commit -m "$(cat <<'EOF' +wip: in-progress eform parity fixes pre-consolidation + +Snapshot of theme/component SCSS work done while iterating against the +Workspace reference. Superseded by the consolidation branch — kept so the +picker UI and backend ThemeVariant plumbing on this branch remain +cherry-pickable. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +- [ ] **Step 3: Push** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git push origin feature/theme-switcher-workspace +``` + +Expected: push succeeds. + +### Task 0.2: Cut the consolidation branch from `stable` + +**Files:** none modified — branch operation only. + +- [ ] **Step 1: Fetch and create branch** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git fetch origin +git checkout stable +git pull origin stable +git checkout -b feature/m3-token-consolidation +``` + +Expected: clean checkout of `stable`, new local branch created. + +- [ ] **Step 2: Push the branch up empty** + +```bash +git push -u origin feature/m3-token-consolidation +``` + +### Task 0.3: Cut the screenshot-baseline branch from `stable` + +**Files:** none yet — branch only. + +- [ ] **Step 1: Branch and push** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git checkout stable +git checkout -b chore/screenshot-baseline +git push -u origin chore/screenshot-baseline +``` + +### Task 0.4: Verify all 18 plugin source repos exist and are clean + +**Files:** none — read-only inspection. + +- [ ] **Step 1: For each plugin in the list above, confirm clean working tree on `stable`** + +```bash +for repo in eform-angular-appointment-plugin eform-angular-basecustomer-plugin \ + eform-angular-chemical-plugin eform-angular-eform-dashboard-plugin \ + eform-angular-greate-belt-plugin eform-angular-insight-dashboard-plugin \ + eform-angular-inventory-plugin eform-angular-items-planning-plugin \ + eform-angular-monitoring-plugin eform-angular-outer-inner-resource-plugin \ + eform-angular-rentableitem-plugin eform-angular-sportfederation-plugin \ + eform-angular-timeplanning-plugin eform-angular-trash-inspection-plugin \ + eform-angular-workflow-plugin eform-angular-work-orders-plugin \ + eform-backendconfiguration-plugin eform-kanban-plugin; do + echo "=== $repo ===" + cd /home/rene/Documents/workspace/microting/$repo + git status -s + git rev-parse --abbrev-ref HEAD +done +``` + +Expected: each prints empty status and `stable` (or whichever is the plugin's canonical branch). + +- [ ] **Step 2: If any plugin has uncommitted work, STOP and ask the user how to handle it** + +Do not proceed past this step until all plugin trees are clean. + +--- + +## Phase 1 — Build screenshot baseline + +### Task 1.1: Add a Playwright capture script + +**Files:** +- Create: `eform-client/screenshots/capture.mjs` +- Create: `eform-client/screenshots/pages.json` + +(Work happens on `chore/screenshot-baseline` branch.) + +- [ ] **Step 1: Switch to baseline branch** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git checkout chore/screenshot-baseline +mkdir -p eform-client/screenshots/baseline +mkdir -p eform-client/screenshots/candidate +mkdir -p eform-client/screenshots/diff +``` + +- [ ] **Step 2: Write `pages.json` defining the page list** + +Create `eform-client/screenshots/pages.json`: + +```json +[ + { "name": "my-eforms", "url": "/", "open": null }, + { "name": "cases-list", "url": "/cases", "open": null }, + { "name": "templates-list", "url": "/searchable-list", "open": null }, + { "name": "sites", "url": "/advanced/sites", "open": null }, + { "name": "workers", "url": "/advanced/workers", "open": null }, + { "name": "profile-settings", "url": "/account-management/account-settings", "open": null }, + { "name": "application-settings", "url": "/application-settings", "open": null }, + { "name": "items-planning", "url": "/plugins/items-planning-pn/plannings", "open": null }, + { "name": "backend-configuration","url": "/plugins/backend-configuration-pn/properties", "open": null }, + { "name": "time-planning", "url": "/plugins/time-planning-pn/plannings", "open": null }, + { "name": "tags-dropdown-open", "url": "/", "open": "tags-dropdown" }, + { "name": "delete-confirm-dialog","url": "/cases", "open": "first-row-delete" } +] +``` + +- [ ] **Step 3: Write the capture script** + +Create `eform-client/screenshots/capture.mjs`: + +```javascript +// Usage: node capture.mjs [--mode=light|dark] +// Reads pages.json, logs in, navigates each page, captures full-page PNG. + +import { chromium } from 'playwright'; +import fs from 'node:fs'; +import path from 'node:path'; + +const args = process.argv.slice(2); +const outDir = args[0]; +const mode = (args.find(a => a.startsWith('--mode=')) || '--mode=light').split('=')[1]; +if (!outDir) { console.error('output dir required'); process.exit(1); } +fs.mkdirSync(outDir, { recursive: true }); + +const pages = JSON.parse(fs.readFileSync(new URL('./pages.json', import.meta.url))); + +const BASE = process.env.E2E_BASE_URL || 'http://localhost:4200'; +const USER = process.env.E2E_USER || 'admin@admin.com'; +const PASS = process.env.E2E_PASS || 'H@ppy2Learn'; + +const browser = await chromium.launch(); +const ctx = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); +const page = await ctx.newPage(); + +await page.goto(BASE + '/login'); +await page.fill('input[name="userName"]', USER); +await page.fill('input[name="password"]', PASS); +await page.click('button[type="submit"]'); +await page.waitForURL((u) => !u.toString().includes('/login'), { timeout: 30000 }); + +if (mode === 'dark') { + // Toggle dark mode through the profile settings UI. + // Inspect actual DOM to confirm the selector before locking this in. + await page.goto(BASE + '/account-management/account-settings'); + await page.click('mat-slide-toggle[formcontrolname="darkTheme"] button'); + await page.waitForTimeout(500); +} + +for (const p of pages) { + await page.goto(BASE + p.url); + await page.waitForLoadState('networkidle'); + if (p.open === 'tags-dropdown') { + await page.click('ng-select[formcontrolname="tagIds"]'); + await page.waitForTimeout(300); + } else if (p.open === 'first-row-delete') { + await page.click('table tbody tr:first-child button[mattooltip*="Delete"]'); + await page.waitForTimeout(300); + } + await page.waitForTimeout(500); // settle animations + const file = path.join(outDir, `${p.name}__${mode}.png`); + await page.screenshot({ path: file, fullPage: true }); + console.log('captured', file); +} + +await browser.close(); +``` + +- [ ] **Step 4: Verify the script syntactically** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +node --check screenshots/capture.mjs +``` + +Expected: no output (success). + +- [ ] **Step 5: Commit** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git add eform-client/screenshots/capture.mjs eform-client/screenshots/pages.json +git commit -m "$(cat <<'EOF' +chore(screenshots): add Playwright capture harness for theme regression + +Captures full-page PNGs of representative app pages (host + 3 plugins) in +light and dark mode. Used to gate the M3 token consolidation work against +visual drift from current stable. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +git push +``` + +### Task 1.2: Add the diff script + +**Files:** +- Create: `eform-client/screenshots/diff.mjs` +- Modify: `eform-client/package.json` (add `pixelmatch` + `pngjs` to devDependencies) + +- [ ] **Step 1: Install pixelmatch + pngjs** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +yarn add --dev pixelmatch pngjs +``` + +Expected: dependencies added, `yarn.lock` updated. + +- [ ] **Step 2: Write the diff script** + +Create `eform-client/screenshots/diff.mjs`: + +```javascript +// Usage: node diff.mjs [--threshold=0.005] +// Compares matching PNGs pair-by-pair, writes per-page diff PNGs, exits non-zero +// if any page exceeds the per-page diff ratio threshold (default 0.5%). + +import fs from 'node:fs'; +import path from 'node:path'; +import { PNG } from 'pngjs'; +import pixelmatch from 'pixelmatch'; + +const [baselineDir, candidateDir, outDir, ...rest] = process.argv.slice(2); +const threshold = Number((rest.find(a => a.startsWith('--threshold=')) || '--threshold=0.005').split('=')[1]); + +if (!baselineDir || !candidateDir || !outDir) { + console.error('usage: diff.mjs [--threshold=0.005]'); + process.exit(2); +} +fs.mkdirSync(outDir, { recursive: true }); + +const files = fs.readdirSync(baselineDir).filter(f => f.endsWith('.png')); +let failures = 0; + +for (const f of files) { + const a = PNG.sync.read(fs.readFileSync(path.join(baselineDir, f))); + const candidatePath = path.join(candidateDir, f); + if (!fs.existsSync(candidatePath)) { + console.log(`MISSING ${f}`); + failures++; + continue; + } + const b = PNG.sync.read(fs.readFileSync(candidatePath)); + if (a.width !== b.width || a.height !== b.height) { + console.log(`SIZE-MISMATCH ${f}: baseline ${a.width}x${a.height}, candidate ${b.width}x${b.height}`); + failures++; + continue; + } + const diff = new PNG({ width: a.width, height: a.height }); + const diffPx = pixelmatch(a.data, b.data, diff.data, a.width, a.height, { threshold: 0.1 }); + const ratio = diffPx / (a.width * a.height); + fs.writeFileSync(path.join(outDir, f), PNG.sync.write(diff)); + const status = ratio > threshold ? 'FAIL' : 'OK '; + console.log(`${status} ${f} ratio=${(ratio * 100).toFixed(3)}%`); + if (ratio > threshold) failures++; +} + +if (failures > 0) { + console.error(`\n${failures} page(s) failed`); + process.exit(1); +} +console.log('\nall pages within tolerance'); +``` + +- [ ] **Step 3: Verify syntactically** + +```bash +node --check screenshots/diff.mjs +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git add eform-client/screenshots/diff.mjs eform-client/package.json eform-client/yarn.lock +git commit -m "chore(screenshots): add pixelmatch diff script with 0.5% threshold + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +### Task 1.3: Capture baseline screenshots from `stable` + +**Files:** +- Create: `eform-client/screenshots/baseline/*.png` (24 files: 12 pages × 2 modes) + +- [ ] **Step 1: Confirm yarn dev server is running on `stable` SCSS** + +User confirmation required: dev server must be serving `stable`'s SCSS, not the dirty `feature/theme-switcher-workspace` state. Restart yarn against the `chore/screenshot-baseline` branch (which is identical to `stable` except for the screenshots dir). + +```bash +# In a separate terminal: +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +yarn start +# Wait for "Compiled successfully" +``` + +- [ ] **Step 2: Run capture in light mode** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +node screenshots/capture.mjs screenshots/baseline --mode=light +``` + +Expected: prints "captured screenshots/baseline/__light.png" for each of the 12 pages. + +- [ ] **Step 3: Run capture in dark mode** + +```bash +node screenshots/capture.mjs screenshots/baseline --mode=dark +``` + +Expected: 12 more "captured" lines. + +- [ ] **Step 4: Visually sanity-check** + +```bash +ls -la eform-client/screenshots/baseline/ | wc -l +# Expected: 26 (24 PNGs + . + ..) +``` + +Open 2-3 PNGs manually to confirm they're not blank. + +- [ ] **Step 5: Commit and push** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git add eform-client/screenshots/baseline/*.png +git commit -m "chore(screenshots): baseline capture from stable + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +--- + +## Phase 2 — Vocabulary (one subagent, sequential) + +### Task 2.1: Switch back to consolidation branch + +- [ ] **Step 1** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git checkout feature/m3-token-consolidation +``` + +### Task 2.2: Dispatch the audit subagent + +**Files:** none yet — subagent produces files in next task. + +- [ ] **Step 1: Dispatch a `general-purpose` subagent with this prompt** + +``` +You are auditing SCSS for an Angular Material 3 token consolidation. Goal: produce +a complete vocabulary of every visual value used across the host app and 18 plugin +source repos. + +Scopes (ripgrep all of these): + - /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client/src/scss/**/*.scss + - /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client/src/app/**/*.scss + - For each of these 18 plugin source repos, the path + /eform-client/src/app/plugins/modules/-pn/**/*.scss: + eform-angular-appointment-plugin, eform-angular-basecustomer-plugin, + eform-angular-chemical-plugin, eform-angular-eform-dashboard-plugin, + eform-angular-greate-belt-plugin, eform-angular-insight-dashboard-plugin, + eform-angular-inventory-plugin, eform-angular-items-planning-plugin, + eform-angular-monitoring-plugin, eform-angular-outer-inner-resource-plugin, + eform-angular-rentableitem-plugin, eform-angular-sportfederation-plugin, + eform-angular-timeplanning-plugin, eform-angular-trash-inspection-plugin, + eform-angular-workflow-plugin, eform-angular-work-orders-plugin, + eform-backendconfiguration-plugin, eform-kanban-plugin + - All under /home/rene/Documents/workspace/microting/ + +Patterns to extract: + - Hex colors (#RGB, #RRGGBB, #RRGGBBAA) + - rgb(), rgba(), hsl(), hsla() + - Hardcoded px/rem values in these properties only: + width, height, min-height, max-height, min-width, max-width, + border-radius, padding, padding-(top|right|bottom|left), + margin, margin-(top|right|bottom|left), gap, row-gap, column-gap, + font-size, line-height, letter-spacing, font-weight, + box-shadow, border, border-(top|right|bottom|left) + +Excludes (do not extract): + - SVG fill/stroke attributes inside Angular templates + - Values 0, auto, inherit, initial, unset, none + - Values inside @keyframes, @media, @supports + +For each unique value, record: token name (you propose), eform-light value, +eform-dark value (read existing _eform.scss for dark-mode equivalents where they +exist; otherwise mark as "TBD-dark" and we'll resolve in review), source file:line +of the FIRST occurrence, and one example call-site selector. + +Output format: + 1. Write a markdown table to + /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md + with columns: Token | Light | Dark | Source | Example. + Group rows under H2 sections: ## Color, ## Typography, ## Shape, ## Sizing, + ## Spacing, ## Elevation. Use H3 sub-sections inside Color (## Surface, + ## State, ## Brand, etc.) if helpful. + 2. After the tables, add a "## Plugin coverage" section listing each plugin + with the count of unique values found in it, so we know which plugins + actually contribute SCSS. + +Naming rules: + - Existing token names in eform-client/src/scss/themes/_eform.scss take + precedence — reuse them. Do not rename. + - New tokens follow kebab-case-with-purpose: --primary, --primary-hover, + --table-row-hover, --nav-item-height, --button-padding-h, etc. + - Avoid numeric suffixes (--color-1) — use semantic names. + +DO NOT edit any SCSS yet. DO NOT populate _eform.scss yet. ONLY produce the +vocabulary doc. Report back with the doc path and total unique-value count. +``` + +- [ ] **Step 2: Wait for subagent completion. Verify the vocabulary doc exists and is non-trivial** + +```bash +ls -la /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md +wc -l /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md +``` + +Expected: file exists, > 200 lines. + +- [ ] **Step 3: Commit the vocabulary doc** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git add docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md +git commit -m "docs: M3 token vocabulary (audit output) + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +### Task 2.3: Vocabulary review gate (USER) + +- [ ] **Step 1: Pause and present the vocabulary doc to the user** + +Ask the user to read `docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md` end-to-end. The user must: + 1. Accept or rename any token they dislike. + 2. Resolve any "TBD-dark" entries. + 3. Confirm the plugin coverage list matches expectations. + +Do not proceed past this step without explicit user approval. + +### Task 2.4: Populate `_eform.scss` exhaustively + +**Files:** +- Modify: `eform-client/src/scss/themes/_eform.scss` + +- [ ] **Step 1: Read the locked vocabulary doc and the current `_eform.scss`** + +- [ ] **Step 2: Rewrite `_eform.scss` so its `colors` map and top-level keys contain every token from the vocabulary** + +For each token in the doc, ensure there's a key in either the top-level map (for shape/sizing/spacing/typography), the `light` sub-map, or the `dark` sub-map. Light values come from the vocabulary; dark values come from the vocabulary's "Dark" column. + +- [ ] **Step 3: Verify SCSS compiles** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +yarn build +``` + +Expected: build succeeds. Any "undefined variable" or "function not found" SCSS errors must be fixed before continuing. + +- [ ] **Step 4: Commit** + +```bash +git add eform-client/src/scss/themes/_eform.scss +git commit -m "feat(theme): populate _eform.scss with full token vocabulary + +Adds every token from the locked vocabulary doc to the eform token map. +Workspace theme + theme-mixin emission updated in subsequent commits. + +Co-Authored-By: Claude Opus 4.6 " +``` + +### Task 2.5: Populate `_theme-mixin.scss` to emit every token as a CSS var + +**Files:** +- Modify: `eform-client/src/scss/themes/_theme-mixin.scss` + +- [ ] **Step 1: For every key in the eform `light`/`dark` sub-maps, emit a `--: #{map.get($mode-colors, )};` line** + +For every top-level non-color key (e.g. `nav-item-height`, `button-padding-h`), emit `--: #{map.get($tokens, )};`. + +- [ ] **Step 2: For each Material component touched by the vocabulary, ensure there's a `mat.-overrides()` call binding the corresponding tokens** + +Example: if vocabulary defines `--button-shape`, ensure `mat.button-overrides((filled-container-shape: map.get($tokens, button-shape), ...))` is present. Refer to the existing `_theme-mixin.scss` calls as a template. + +- [ ] **Step 3: Verify build** + +```bash +cd eform-client && yarn build +``` + +Expected: success. + +- [ ] **Step 4: Commit** + +```bash +git add eform-client/src/scss/themes/_theme-mixin.scss +git commit -m "feat(theme): emit every vocabulary token as a CSS custom property + +Every key in the eform token map now appears as -- in :root, +making downstream component SCSS able to consume them via var(--key). + +Co-Authored-By: Claude Opus 4.6 " +``` + +### Task 2.6: Mirror the key set into `_workspace.scss` skeleton + +**Files:** +- Modify: `eform-client/src/scss/themes/_workspace.scss` + +- [ ] **Step 1: Open `_workspace.scss` and ensure it contains the exact same keys as `_eform.scss`** + +Values can be placeholders (e.g. duplicate the eform values) — they will be re-tuned in a follow-up. The point is: no key may be missing, or `apply-theme($workspace-tokens, $mode)` will emit empty CSS vars and break the workspace theme. + +- [ ] **Step 2: Verify build** + +```bash +cd eform-client && yarn build +``` + +- [ ] **Step 3: Commit** + +```bash +git add eform-client/src/scss/themes/_workspace.scss +git commit -m "feat(theme): mirror full key set into workspace token map + +Workspace map now matches eform key-for-key (placeholder values). +Re-tuning the workspace look is a follow-up commit. + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +--- + +## Phase 3 — Mechanical replace + +### Task 3.1: Initialize the gaps log + +**Files:** +- Create: `gaps.md` (project root, transient) + +- [ ] **Step 1: Create empty gaps log** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +cat > gaps.md <<'EOF' +# Gaps log — phase 2 mechanical replace + +Append-only. One row per hardcoded value with no matching token. + +| Subagent | File:line | Value | Surrounding context | +|---|---|---|---| +EOF +git add gaps.md +git commit -m "chore: add transient gaps log for phase 2 + +Co-Authored-By: Claude Opus 4.6 " +``` + +### Task 3.2: Dispatch host-global SCSS subagent + +**Files (target):** +- Modify: `eform-client/src/scss/components/**/*.scss` +- Modify: `eform-client/src/scss/libs/**/*.scss` (excluding `themes/`) +- Modify: `eform-client/src/scss/utilities/**/*.scss` +- Modify: `eform-client/src/scss/styles.scss` + +- [ ] **Step 1: Dispatch a `general-purpose` subagent with this prompt (verbatim, no paraphrasing)** + +``` +You are doing mechanical token replacement. Read the locked vocabulary at +/home/rene/Documents/workspace/microting/reference/eform-angular-frontend/docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md + +Your scope is HOST GLOBAL ONLY: + - eform-client/src/scss/components/**/*.scss + - eform-client/src/scss/libs/**/*.scss (EXCLUDING the themes/ subdirectory) + - eform-client/src/scss/utilities/**/*.scss + - eform-client/src/scss/styles.scss + +For every hardcoded value in your scope that maps to a vocabulary token, replace +it with var(--, ) so the fallback preserves the old +value if the var is undefined. + +Examples: + border-radius: 4px; -> border-radius: var(--dropdown-panel-radius, 4px); + background: #289694; -> background: var(--primary, #289694); + color: #FFFFFF; -> color: var(--on-primary, #FFFFFF); + height: 48px; (in a button) -> height: var(--primary-button-height, 48px); + +Forbidden actions: + - Inventing new tokens (if a value has no matching token, append a row to + /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/gaps.md + and skip that line). + - Tweaking any value. + - Removing or adding !important. + - Reordering selectors or rules. + - Touching themes/_eform.scss, themes/_workspace.scss, themes/_theme-mixin.scss. + - Touching anything outside your scope (no app/, no plugins). + +After every file edit, verify yarn still builds (cd eform-client && yarn build). +If a build fails, revert your last edit and append the failing case to gaps.md. + +Report at the end: + - List of files modified + - Count of substitutions made + - Number of gaps appended + +Do NOT commit. The orchestrator will commit after review. +``` + +- [ ] **Step 2: After subagent completes, review `git diff --stat` and `gaps.md`** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git diff --stat +cat gaps.md +yarn --cwd eform-client build +``` + +Expected: build passes, diff is mechanical (no value changes). + +- [ ] **Step 3: Spot-check 3 random files in the diff for sanity** + +For each of 3 files, confirm the only change is `` → `var(--token, )` — no logic, no rule reordering. + +- [ ] **Step 4: Commit** + +```bash +git add eform-client/src/scss/ +git commit -m "refactor(scss): tokenize host-global SCSS + +Mechanical replace of hardcoded color/size/spacing/radius values in +scss/components, scss/libs (excluding themes), scss/utilities, and +styles.scss with var(--token, fallback) references against the locked +vocabulary. No visual or behavioral changes intended. + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +### Task 3.3: Dispatch host-components subagent + +**Files (target):** +- Modify: `eform-client/src/app/**/*.scss` excluding `src/app/plugins/modules/**` + +- [ ] **Step 1: Dispatch a `general-purpose` subagent with the same prompt as Task 3.2 BUT scope replaced with** + +``` +Your scope is HOST COMPONENTS ONLY: + - eform-client/src/app/**/*.scss + EXCLUDING eform-client/src/app/plugins/modules/** +``` + +All other rules (forbidden actions, gaps log, build verify) identical to Task 3.2. + +- [ ] **Step 2: Review diff + gaps + build, same as Task 3.2 Step 2** + +- [ ] **Step 3: Spot-check 3 random files** + +- [ ] **Step 4: Commit** + +```bash +git add eform-client/src/app/ +git commit -m "refactor(scss): tokenize host component SCSS + +Mechanical replace across eform-client/src/app/**/*.scss (excluding +plugin modules) against the locked vocabulary. No visual changes +intended. + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +### Task 3.4: Resolve any gaps before plugin work + +- [ ] **Step 1: Read `gaps.md`** + +If empty, skip to Task 3.5. + +- [ ] **Step 2: For each gap, decide:** + + - **Add a token:** if the value belongs to the visual identity, add it to `_eform.scss`, `_workspace.scss` (placeholder), `_theme-mixin.scss` (CSS var emission), and the vocabulary doc. Then re-dispatch the relevant subagent on just that file(s) to replace the gap. + - **Mark as out-of-scope:** if the value is genuinely not theme-related (e.g. an offset for a specific overlay positioning), document the rationale next to the gap row in `gaps.md` (move it under a `## Out of scope` section) and leave the value hardcoded. + +- [ ] **Step 3: Commit each token-add as its own commit** + +```bash +git add eform-client/src/scss/themes/_eform.scss \ + eform-client/src/scss/themes/_workspace.scss \ + eform-client/src/scss/themes/_theme-mixin.scss \ + docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md +git commit -m "feat(theme): add to vocabulary (gap resolution) + + + +Co-Authored-By: Claude Opus 4.6 " +``` + +- [ ] **Step 4: Re-run subagent on the file containing the gap** + +Dispatch a focused subagent with the same forbidden-actions list, scope limited to the single file, plus an instruction: "the previous gap at : for value is now resolved by token --. Replace it now." + +- [ ] **Step 5: Commit and push** + +```bash +git add +git commit -m "refactor(scss): resolve gap for + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +### Task 3.5: Dispatch plugin subagents in parallel + +**Files (target):** +- Modify: `eform-client/src/app/plugins/modules/-pn/**/*.scss` for each plugin's host-app copy (since edits happen in dev mode in the host app). + +This requires dev mode. Confirm with the user that they are in full dev mode (per CLAUDE.md session-start gate). If not, abort and switch dev mode first. + +- [ ] **Step 1: Confirm dev mode** + +Ask the user: "Are you in full dev mode (fullblownsetup.py)? Plugin SCSS edits happen in the host app and require local project references." + +Do not proceed without confirmation. + +- [ ] **Step 2: Dispatch ONE subagent per plugin, in parallel (single message, multiple Agent tool calls)** + +For each plugin in the 18-plugin list, dispatch a subagent with this prompt template (substitute ``): + +``` +You are doing mechanical token replacement for the plugin's SCSS. + +Vocabulary: /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md + +Scope (single plugin): + /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client/src/app/plugins/modules/-pn/**/*.scss + +Same rules as the host subagents: + - Replace hardcoded values with var(--token, fallback). + - No new tokens — append to /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/gaps.md if blocked. + - No value tweaks, no reordering, no removing !important. + - Verify build (cd eform-client && yarn build) after each file. + +If the plugin has NO .scss files in its module directory, report "no files in scope" +and exit cleanly. + +Report: files modified, substitutions made, gaps appended. +Do NOT commit. +``` + +NOTE: the plugin module directory naming may not be `-pn` exactly — confirm the actual directory by listing `eform-client/src/app/plugins/modules/` first and pass each subagent its actual directory name. + +- [ ] **Step 3: Wait for all subagents to complete. Aggregate diffs and gaps** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git diff --stat eform-client/src/app/plugins/ +cat gaps.md # any new gaps from this round? +yarn --cwd eform-client build +``` + +If new gaps appeared, return to Task 3.4 to resolve them, then re-dispatch the affected plugin subagents. + +- [ ] **Step 4: Commit the plugin SCSS edits in the host app (one commit per plugin) — these will be back-synced shortly** + +```bash +# For each plugin: +git add eform-client/src/app/plugins/modules/-pn/ +git commit -m "refactor(scss): tokenize plugin SCSS + +Mechanical replace against the locked vocabulary. Will be back-synced +to the plugin source repo via devgetchanges.sh. + +Co-Authored-By: Claude Opus 4.6 " +``` + +- [ ] **Step 5: Push host branch** + +```bash +git push +``` + +### Task 3.6: Back-sync plugin edits to source repos + +For each plugin that had SCSS edits in Task 3.5: + +- [ ] **Step 1: Switch to the plugin source repo and create the consolidation branch** + +```bash +cd /home/rene/Documents/workspace/microting/ +git checkout stable +git pull origin stable +git checkout -b chore/m3-token-consolidation +``` + +- [ ] **Step 2: Run `devgetchanges.sh`** + +```bash +./devgetchanges.sh +``` + +Expected: SCSS files updated from the host app copy. + +- [ ] **Step 3: Reset build artifacts (per CLAUDE.md)** + +```bash +git checkout -- '*.csproj' '*.conf.ts' '*.xlsx' '*.docx' 2>/dev/null || true +``` + +- [ ] **Step 4: Verify only intended files changed** + +```bash +git status +git diff --stat +``` + +Expected: only `.scss` files under `eform-client/src/app/plugins/modules/-pn/` (or equivalent) modified. + +- [ ] **Step 5: Stage explicitly and commit** + +```bash +# Stage only the .scss files surfaced by Step 4 — list them by name, no `git add .` +git add +git commit -m "refactor(scss): tokenize plugin SCSS against host vocabulary + +Replaces hardcoded color/size/spacing/radius values with var(--token, +fallback) references. Vocabulary lives in eform-angular-frontend at +docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md. + +Co-Authored-By: Claude Opus 4.6 " +git push -u origin chore/m3-token-consolidation +``` + +- [ ] **Step 6: Repeat Steps 1-5 for every plugin in the 18-plugin list that had edits** + +### Task 3.7: Clean up the gaps log + +- [ ] **Step 1: Confirm `gaps.md` is empty or contains only documented out-of-scope entries** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +cat gaps.md +``` + +- [ ] **Step 2: If empty, delete; if has out-of-scope entries, move into the vocabulary doc as an "Out of scope values" appendix** + +- [ ] **Step 3: Commit** + +```bash +git rm gaps.md # or git add gaps.md if moved into vocabulary doc +git add docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md +git commit -m "chore: close phase 2 gaps log + +Co-Authored-By: Claude Opus 4.6 " +git push +``` + +--- + +## Phase 4 — Screenshot regression + +### Task 4.1: Capture candidate screenshots from consolidation branch + +**Files:** +- Create: `eform-client/screenshots/candidate/*.png` (24 files) + +- [ ] **Step 1: Restart yarn dev server against `feature/m3-token-consolidation`** + +```bash +# In separate terminal: +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +# Kill any running yarn start, then: +yarn start +# Wait for "Compiled successfully" +``` + +- [ ] **Step 2: Run capture in light mode** + +```bash +node screenshots/capture.mjs screenshots/candidate --mode=light +``` + +- [ ] **Step 3: Run capture in dark mode** + +```bash +node screenshots/capture.mjs screenshots/candidate --mode=dark +``` + +- [ ] **Step 4: Verify file count** + +```bash +ls eform-client/screenshots/candidate/*.png | wc -l +# Expected: 24 +``` + +### Task 4.2: Run the diff + +- [ ] **Step 1: Execute diff script** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend/eform-client +node screenshots/diff.mjs screenshots/baseline screenshots/candidate screenshots/diff +echo "exit: $?" +``` + +Expected on success: each page prints "OK ratio=0.xxx%", final line "all pages within tolerance", exit 0. + +Expected on failure: one or more "FAIL" lines, exit 1. + +### Task 4.3: Fix loop + +- [ ] **Step 1: For each FAIL line, open the diff PNG** + +```bash +xdg-open eform-client/screenshots/diff/.png +``` + +- [ ] **Step 2: Identify the visual diff cause** + + - **Wrong token value:** the vocabulary captured the wrong color/size for the eform theme. Fix in `_eform.scss`. Re-run capture + diff. + - **Missed hardcode:** a SCSS file slipped through phase 2. Find it (`grep -rn "" eform-client/src/`), tokenize manually, commit, re-run. + - **Anti-aliasing noise above threshold:** if a page is failing at 0.6-0.8% with no visible difference to the eye, raise the threshold for that page only by adding it to a per-page override map in `diff.mjs` (don't bump the global threshold). + +- [ ] **Step 3: After every fix, commit** + +```bash +git add +git commit -m "fix(theme): on + +Co-Authored-By: Claude Opus 4.6 " +``` + +- [ ] **Step 4: Re-run Task 4.1 (recapture) and Task 4.2 (diff) until clean** + +### Task 4.4: User sign-off + +- [ ] **Step 1: Present candidate vs baseline side-by-side to the user** + +For 5-6 representative pages, open baseline and candidate PNGs side-by-side. Ask the user to confirm visual equivalence. + +- [ ] **Step 2: Wait for user explicit "approved" / "looks identical"** + +Do not proceed past this step without it. + +--- + +## Phase 5 — PR coordination & rebase + +### Task 5.1: Open PRs + +- [ ] **Step 1: Open the host PR** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +gh pr create --base stable --title "M3 token consolidation" --body "$(cat <<'EOF' +## Summary +- Extracts every hardcoded color, size, radius, spacing, and elevation value across host SCSS into the eform token vocabulary. +- Replaces all hardcodes with var(--token, fallback) references. +- Workspace token map mirrored key-for-key (placeholder values for follow-up tuning). +- Adds Playwright screenshot capture + pixelmatch diff harness as a permanent regression guard. + +## Verification +- Screenshot diff vs stable: all pages within 0.5% per-page tolerance. +- yarn build, lint, and dotnet build all pass. +- No mat.m2-* references remain in scss/. + +## Test plan +- [x] Light + dark mode capture matches stable on 12 pages +- [x] No visible regressions on user-eyeball pass +- [ ] Reviewer eyeballs 3-5 candidate screenshots in screenshots/diff/ + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +- [ ] **Step 2: For each plugin source repo with a `chore/m3-token-consolidation` branch, open a PR** + +```bash +# Repeat for each affected plugin: +cd /home/rene/Documents/workspace/microting/ +gh pr create --base stable --title "M3 token consolidation" --body "$(cat <<'EOF' +## Summary +- Replaces hardcoded SCSS values in this plugin with var(--token, fallback) references against the host eform-angular-frontend vocabulary. +- No visual or behavioral changes. + +## Test plan +- [x] Build passes +- [ ] Visual check via host app screenshot diff (covered in eform-angular-frontend PR) + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +### Task 5.2: Coordinate merge + +- [ ] **Step 1: Wait for review approval on every PR** + +- [ ] **Step 2: Merge plugin PRs first** + +For each plugin PR, merge via `gh pr merge --merge`. Plugins consume the host vocabulary — they're safe to merge once the host vocabulary is approved. + +- [ ] **Step 3: Merge host PR last** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +gh pr merge feature/m3-token-consolidation --merge +``` + +### Task 5.3: Rebase the picker UI branch onto consolidated stable + +- [ ] **Step 1: Pull updated stable** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git checkout stable +git pull origin stable +``` + +- [ ] **Step 2: Rebase the picker branch** + +```bash +git checkout feature/theme-switcher-workspace +git rebase stable +``` + +Expected: conflicts in `_eform.scss`, `_theme-mixin.scss`, `_workspace.scss`, possibly `navigation.component.scss`, etc. Resolve each by KEEPING the consolidated stable version, then re-apply only the picker UI + state plumbing changes. + +- [ ] **Step 3: Verify build and push** + +```bash +yarn --cwd eform-client build +git push --force-with-lease origin feature/theme-switcher-workspace +``` + +### Task 5.4: Tag the consolidation point + +- [ ] **Step 1: Tag stable** + +```bash +cd /home/rene/Documents/workspace/microting/reference/eform-angular-frontend +git checkout stable +git pull +git tag -a m3-consolidation-complete -m "M3 token consolidation merged" +git push origin m3-consolidation-complete +``` + +This is the named "known-good" point that future theme work (workspace tuning, additional themes) builds on. diff --git a/docs/superpowers/specs/2026-04-14-m3-token-consolidation-design.md b/docs/superpowers/specs/2026-04-14-m3-token-consolidation-design.md new file mode 100644 index 0000000000..c7052a9c98 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-m3-token-consolidation-design.md @@ -0,0 +1,179 @@ +# M3 Token Consolidation — Design + +## Context + +The eForm frontend is mid-migration to Angular Material 3. The existing branch `feature/theme-switcher-workspace` (PR #7756, partially merged into `stable`) introduced the M3 plumbing — a token map in `_eform.scss`, an `apply-theme` mixin in `_theme-mixin.scss`, the `ThemeVariant` backend column, NgRx state, and an admin-settings picker UI. It also introduced a second tentative theme (`_workspace.scss`). + +What it did *not* do: extract every hardcoded color, radius, size, and spacing value from the existing SCSS into the token vocabulary. As a result, swapping themes (or even just refactoring the eform default look) keeps surfacing hidden hardcoded values that diverge from the original visual identity. We have spent multiple iterations chasing visual regressions one-by-one through Playwright comparisons. + +The user's standing feedback: when there is a complex but correct solution, propose it. No easy fixes. Fix it for good. + +This spec defines a complete consolidation: audit *every* hardcoded visual value across the host app **and** all plugin source repos, lift them into a single token vocabulary, mechanically replace the hardcodes with `var(--…)` references, and gate the merge on a screenshot-regression diff against current production. + +## Goals + +1. After consolidation, the eform theme renders pixel-identical to today's `stable` (within anti-aliasing noise). +2. Every visual value used anywhere in `eform-angular-frontend` or any `eform-angular-*-plugin` repo is either (a) referenced via a `var(--…)` declared in the token vocabulary, or (b) explicitly classified as out-of-scope (e.g. SVG paint, animation keyframes). +3. Adding a new theme variant becomes a token-map swap with zero per-component SCSS edits. +4. A Playwright screenshot-diff regression suite exists as a reusable guard for future theme work. + +## Non-goals + +- Re-tuning the workspace theme tokens (separate follow-up). +- Visual changes to the eform theme (consolidation must be visually neutral). +- Changes to component DOM structure or Angular templates. +- Migrating motion (durations / easings) unless the audit finds in-scope usages. + +## Approach: token-vocabulary-first, then mechanical replace + +Three sequential phases, each with a hard review gate. + +### Phase 1 — Vocabulary + +A single subagent audits every targeted SCSS file and produces: + +- **Vocabulary document** at `docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md`. Per token: name, eform-light value, eform-dark value, source file:line, one example call site. Categories: color, typography, shape, sizing, spacing, elevation, (motion if applicable). +- **Populated `_eform.scss`** containing every token from the doc. +- **Populated `_theme-mixin.scss`** that emits a `--` CSS custom property for every map key, plus the relevant `mat.*-overrides()` calls. +- **Skeleton `_workspace.scss`** with the same key set and placeholder values. + +#### Audit method + +Ripgrep across: +- `eform-client/src/scss/**/*.scss` +- `eform-client/src/app/**/*.scss` +- For each plugin source repo (enumerated via `ls /home/rene/Documents/workspace/microting/eform-angular-*-plugin`): `/eform-client/src/app/plugins/modules/-pn/**/*.scss` + +Patterns: hex colors, `rgb*()`/`hsl*()`, hardcoded `px`/`rem` for sizing/radii/spacing/typography in known properties (`width`, `height`, `min-height`, `max-height`, `border-radius`, `padding`, `margin`, `gap`, `font-size`, `line-height`, `letter-spacing`, `box-shadow`). + +Excludes: SVG `fill`/`stroke` attributes inside templates, `0`/`auto`/`inherit`, values inside `@keyframes`, values inside `@media`/`@supports` breakpoint expressions. + +#### Review gate + +User reads the vocabulary doc and the populated `_eform.scss`. Naming, categorization, and value-conflation issues are fixed in phase 1 — not later. + +### Phase 2 — Mechanical replace + +Parallel subagents, partitioned by scope: + +1. Host global — `eform-client/src/scss/components/**`, `scss/libs/**`, `scss/utilities/**`, `styles.scss`. +2. Host components — `eform-client/src/app/**/*.scss` excluding `src/app/plugins/modules/**`. +3. One subagent per plugin source repo (list produced by phase 1). + +#### Subagent contract + +- Read-only inputs: locked vocabulary doc, file list, forbidden-actions list. +- Allowed actions: replace hardcoded values with `var(--, )` where `` is the literal value being replaced. +- Forbidden actions: invent new tokens, tweak any value, remove `!important`, restructure rules, reorder selectors. +- Blocked-on-gap protocol: if a value has no matching token, the subagent appends a row to `gaps.md` (file path, line, value, surrounding context) and continues with the rest. Gaps are resolved by the orchestrator in a single supervised edit to `_eform.scss` + `_theme-mixin.scss` + the vocabulary doc, then blocked subagents are restarted on their gap files. + +#### Plugin sync + +Plugin subagents work in dev-mode in the host app at `eform-client/src/app/plugins/modules/-pn/`. After each plugin subagent reports complete, the orchestrator runs `devgetchanges.sh` from the source plugin repo, runs the standard reset of build artifacts (`git checkout *.csproj *.conf.ts *.xlsx *.docx`), reviews `git status` against the subagent's reported file list, and commits on the plugin's `chore/m3-token-consolidation` branch. + +#### Concurrency safety + +Phase 1 already locked the vocabulary, so no two subagents can disagree on token names or values. The only shared mutable state during phase 2 is the append-only `gaps.md`. + +### Phase 3 — Screenshot regression + +#### Baseline capture (before any consolidation work begins) + +Playwright script captures full-page PNGs of N representative pages on `stable`: +- My eForms (root) +- Cases list, Case edit +- Templates list, Template designer +- Sites +- Workers +- Profile Settings, Application Settings +- Items Planning list (plugin) +- Backend Configuration (plugin) +- Time Planning (plugin) +- An open delete-confirmation dialog +- An open `mat-datepicker` +- An open `ng-select` dropdown +- A sorted, paginated data table +- A focused `ngx-editor` field +- A visible toastr + +Each captured in light mode, then dark mode. Output: `eform-client/screenshots/baseline/__.png`. Stored on a dedicated `chore/screenshot-baseline` branch (kept out of the consolidation PR). + +#### Diff run (after phases 1 + 2 complete) + +Same script targets the consolidation branch; outputs to `eform-client/screenshots/candidate/`. A `pixelmatch`-driven diff script writes per-page diff PNGs to `screenshots/diff/` and fails if any page's diff ratio exceeds **0.5%** (anti-aliasing tolerance). + +#### Fix loop + +Any failing page → inspect diff PNG → identify cause: +- Wrong token value → fix in `_eform.scss`. +- Missed hardcode → amend the file (and update the vocabulary if needed). + +Re-run diff until clean. + +#### Sign-off + +User does a final eyeball pass on candidate screenshots once the automated diff passes. Final go/no-go is the user's. + +## Branch & commit strategy + +### Step 0 — preserve current state + +Commit and push the current dirty state on `feature/theme-switcher-workspace`. It has the M3 plumbing, the picker UI, and the backend `ThemeVariant` flow. Do not merge into `stable`. The branch stays alive as a reference for cherry-picking the picker UI + state plumbing onto the consolidated branch at the end. + +### New branches + +- `feature/m3-token-consolidation` cut from `stable` on `eform-angular-frontend`. +- `chore/m3-token-consolidation` cut from `stable` on each affected plugin source repo (list determined by phase 1 audit). +- `chore/screenshot-baseline` cut from `stable` on `eform-angular-frontend`, holds only the baseline PNGs and the capture script. + +### End state + +- One PR on `eform-angular-frontend` (host app + global SCSS + `_eform.scss` + `_theme-mixin.scss` + skeleton `_workspace.scss`). +- N PRs on the affected plugin repos. +- Once all PRs are screenshot-clean, merge in coordinated batch (host PR last). +- Then rebase `feature/theme-switcher-workspace` onto the new `stable` (with consolidation merged) and continue workspace-theme tuning on top of a clean foundation. + +## Files created / modified + +### Created + +- `docs/superpowers/specs/2026-04-14-eform-token-vocabulary.md` (phase 1 output) +- `eform-client/src/scss/themes/_workspace.scss` (skeleton with same keys) +- `eform-client/screenshots/` directory with baseline and capture scripts +- `gaps.md` (transient, deleted at end of phase 2) + +### Rewritten + +- `eform-client/src/scss/themes/_eform.scss` — exhaustive token map. +- `eform-client/src/scss/themes/_theme-mixin.scss` — full CSS-var emission and `mat.*-overrides()` set. + +### Modified (mechanical replace, no value changes) + +- All SCSS under `eform-client/src/scss/components/`, `eform-client/src/scss/libs/`, `eform-client/src/scss/utilities/`, `eform-client/src/scss/styles.scss`. +- All SCSS under `eform-client/src/app/**/*.scss`. +- All SCSS under each affected plugin's `eform-client/src/app/plugins/modules/-pn/**/*.scss`. + +## Verification + +### Static checks + +- `cd eform-client && npm run lint && npm run build --configuration=production` — succeeds with zero new warnings. +- `grep -r 'mat\.m2-' eform-client/src/scss/` — empty. +- After phase 2, hardcoded color/size scan returns either an empty result or only explicitly out-of-scope categories (SVG paint, animation keyframes). + +### Runtime checks + +- Playwright screenshot-diff against `stable` baseline passes (≤ 0.5% per page). +- Manual eyeball pass on all captured screenshots in light + dark. +- Theme-switcher (once rebased on top) still toggles `body.theme-eform` / `body.theme-workspace` correctly and the workspace theme renders with its placeholder values without breaking layout. + +### Regression guard going forward + +The screenshot suite stays in the repo. Any future PR touching `_eform.scss`, `_theme-mixin.scss`, or any tokenized component SCSS runs the diff in CI as a gate. + +## Risks & mitigations + +- **Plugin enumeration is wrong / a plugin gets missed.** Mitigation: phase 1 audit explicitly lists every plugin source repo found via `ls /home/rene/Documents/workspace/microting/eform-angular-*-plugin`, and the user reviews the list at the phase 1 gate. +- **Vocabulary churn during phase 2 (many gaps).** Mitigation: gap-resolution is a one-edit-at-a-time supervised step; each gap also points to a phase 1 audit miss to reflect on. If gap volume is high, halt phase 2 and re-open phase 1 instead of patching iteratively. +- **Screenshot diff false positives (anti-aliasing on font hinting).** Mitigation: 0.5% per-page tolerance; dedicated review of any page that triggers a near-threshold fail. +- **Long branch life across many plugin repos.** Mitigation: do plugin work last (phase 2 step), since plugins consume the host's vocabulary and host SCSS lands first. Plugin PRs can be reviewed and merged in parallel once host PR is approved. diff --git a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Cms/CmsPublicConfigModel.cs b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Cms/CmsPublicConfigModel.cs index b2e17a4a8f..fc2a703346 100644 --- a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Cms/CmsPublicConfigModel.cs +++ b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Cms/CmsPublicConfigModel.cs @@ -24,4 +24,5 @@ public class CmsPublicConfigModel { public bool IsCmsEnabled { get; set; } public bool IsMenuSticky { get; set; } + public string ThemeVariant { get; set; } = "eform"; } diff --git a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AdminSettingsModel.cs b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AdminSettingsModel.cs index e933dbd420..f8cf4ab865 100644 --- a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AdminSettingsModel.cs +++ b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AdminSettingsModel.cs @@ -34,4 +34,5 @@ public class AdminSettingsModel public SendGridSettingsModel SendGridSettingsModel { get; set; } public string SiteLink { get; set; } public string AssemblyVersion { get; set; } + public string ThemeVariant { get; set; } = "eform"; } \ No newline at end of file diff --git a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AppearanceSettings.cs b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AppearanceSettings.cs new file mode 100644 index 0000000000..5eaed26881 --- /dev/null +++ b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/Admin/AppearanceSettings.cs @@ -0,0 +1,6 @@ +namespace eFormAPI.Web.Infrastructure.Models.Settings.Admin; + +public class AppearanceSettings +{ + public string ThemeVariant { get; set; } = "eform"; +} diff --git a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/User/UserSettingsModel.cs b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/User/UserSettingsModel.cs index bdaea0d910..bb0f3a1125 100644 --- a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/User/UserSettingsModel.cs +++ b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Settings/User/UserSettingsModel.cs @@ -28,6 +28,7 @@ public class UserSettingsModel public string Locale { get; set; } public int LanguageId { get; set; } public bool DarkTheme { get; set; } + public string ThemeVariant { get; set; } public string Formats { get; set; } public string TimeZone { get; set; } public string LoginRedirectUrl { get; set; } diff --git a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Users/UserInfoViewModel.cs b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Users/UserInfoViewModel.cs index 540f6b9a25..eac1df4c76 100644 --- a/eFormAPI/eFormAPI.Web/Infrastructure/Models/Users/UserInfoViewModel.cs +++ b/eFormAPI/eFormAPI.Web/Infrastructure/Models/Users/UserInfoViewModel.cs @@ -40,6 +40,7 @@ public class UserInfoViewModel public string Formats { get; set; } public string TimeZone { get; set; } public bool DarkTheme { get; set; } + public string ThemeVariant { get; set; } public bool IsDeviceUser { get; set; } public string ArchiveSoftwareVersion { get; set; } public string ArchiveModel { get; set; } diff --git a/eFormAPI/eFormAPI.Web/Program.cs b/eFormAPI/eFormAPI.Web/Program.cs index dd73c8ef60..751b48a44c 100644 --- a/eFormAPI/eFormAPI.Web/Program.cs +++ b/eFormAPI/eFormAPI.Web/Program.cs @@ -272,6 +272,30 @@ public static void MigrateDb(IHost host) var logger = scope.ServiceProvider.GetRequiredService>(); logger.LogError(e, "Error while adding missing admin settings to all menu items"); } + + try + { + var connectionStrings = + scope.ServiceProvider.GetRequiredService>(); + if (connectionStrings.Value.DefaultConnection != "...") + { + const string key = "AppearanceSettings:ThemeVariant"; + if (!dbContext.ConfigurationValues.Any(x => x.Id == key)) + { + dbContext.ConfigurationValues.Add(new Microting.EformAngularFrontendBase.Infrastructure.Data.Entities.EformConfigurationValue + { + Id = key, + Value = "eform" + }); + dbContext.SaveChanges(); + } + } + } + catch (Exception e) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(e, "Error while seeding AppearanceSettings:ThemeVariant"); + } } } diff --git a/eFormAPI/eFormAPI.Web/Services/AccountService.cs b/eFormAPI/eFormAPI.Web/Services/AccountService.cs index 721fff4412..1a898e5e98 100644 --- a/eFormAPI/eFormAPI.Web/Services/AccountService.cs +++ b/eFormAPI/eFormAPI.Web/Services/AccountService.cs @@ -101,6 +101,7 @@ public async Task> GetUserSettings() var timeZone = string.IsNullOrEmpty(user.TimeZone) ? "Europe/Copenhagen" : user.TimeZone; var formats = string.IsNullOrEmpty(user.Formats) ? "de-DE" : user.Formats; var darkTheme = user.DarkTheme; + var themeVariant = string.IsNullOrEmpty(user.ThemeVariant) ? "eform" : user.ThemeVariant; var locale = string.IsNullOrEmpty(user.Locale) ? "da" : user.Locale; var core = await coreHelper.GetCore(); var dbContextHelper = core.DbContextHelper; @@ -121,6 +122,7 @@ public async Task> GetUserSettings() Locale = locale, LanguageId = languageId, DarkTheme = darkTheme, + ThemeVariant = themeVariant, Formats = formats, TimeZone = timeZone, LoginRedirectUrl = securityGroupRedirectLink, @@ -158,6 +160,10 @@ public async Task UpdateUserSettings(UserSettingsModel model) user.TimeZone = model.TimeZone; user.Formats = model.Formats; user.DarkTheme = model.DarkTheme; + if (!string.IsNullOrEmpty(model.ThemeVariant)) + { + user.ThemeVariant = model.ThemeVariant; + } var updateResult = await userManager.UpdateAsync(user); if (!updateResult.Succeeded) { diff --git a/eFormAPI/eFormAPI.Web/Services/AdminService.cs b/eFormAPI/eFormAPI.Web/Services/AdminService.cs index 96da3bf5c3..009f0e1ad1 100644 --- a/eFormAPI/eFormAPI.Web/Services/AdminService.cs +++ b/eFormAPI/eFormAPI.Web/Services/AdminService.cs @@ -83,6 +83,7 @@ public async Task>> Index(UserInfoR x.user.TimeZone, x.user.Formats, x.user.DarkTheme, + x.user.ThemeVariant, x.user.ArchiveModel, x.user.ArchiveManufacturer, x.user.ArchiveOsVersion, @@ -116,6 +117,7 @@ public async Task>> Index(UserInfoR UserName = x.UserName, Email = x.Email, DarkTheme = x.DarkTheme, + ThemeVariant = x.ThemeVariant, Language = x.Locale, TimeZone = x.TimeZone, Formats = x.Formats, diff --git a/eFormAPI/eFormAPI.Web/Services/CmsService.cs b/eFormAPI/eFormAPI.Web/Services/CmsService.cs index a7bedb161c..0ead123ccd 100644 --- a/eFormAPI/eFormAPI.Web/Services/CmsService.cs +++ b/eFormAPI/eFormAPI.Web/Services/CmsService.cs @@ -25,7 +25,9 @@ namespace eFormAPI.Web.Services; using System.Linq; using System.Threading.Tasks; using Abstractions; +using Hosting.Helpers.DbOptions; using Infrastructure.Models.Cms; +using Infrastructure.Models.Settings.Admin; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microting.EformAngularFrontendBase.Infrastructure.Data; @@ -34,6 +36,7 @@ namespace eFormAPI.Web.Services; public class CmsService( ILogger logger, + IDbOptions appearanceSettings, BaseDbContext dbContext) : ICmsService { // ── Pages ──────────────────────────────────────────────────────────────── @@ -351,7 +354,10 @@ public async Task> GetPublicConfig() return new OperationDataResult(true, new CmsPublicConfigModel { IsCmsEnabled = settings?.IsCmsEnabled ?? false, - IsMenuSticky = settings?.IsMenuSticky ?? false + IsMenuSticky = settings?.IsMenuSticky ?? false, + ThemeVariant = string.IsNullOrEmpty(appearanceSettings.Value.ThemeVariant) + ? "eform" + : appearanceSettings.Value.ThemeVariant }); } catch (Exception ex) diff --git a/eFormAPI/eFormAPI.Web/Services/SettingsService.cs b/eFormAPI/eFormAPI.Web/Services/SettingsService.cs index 6954b6eb2a..5eb6d8236d 100644 --- a/eFormAPI/eFormAPI.Web/Services/SettingsService.cs +++ b/eFormAPI/eFormAPI.Web/Services/SettingsService.cs @@ -60,6 +60,7 @@ public class SettingsService( IDbOptions applicationSettings, IDbOptions loginPageSettings, IDbOptions headerSettings, + IDbOptions appearanceSettings, IDbOptions emailSettings, IEFormCoreService coreHelper, ILocalizationService localizationService, @@ -435,7 +436,10 @@ public async Task> GetAdminSettings() HttpServerAddress = await core.GetSdkSetting(Settings.httpServerAddress) }, SiteLink = await core.GetSdkSetting(Settings.httpServerAddress), - AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString() + AssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(), + ThemeVariant = string.IsNullOrEmpty(appearanceSettings.Value.ThemeVariant) + ? "eform" + : appearanceSettings.Value.ThemeVariant }; return new OperationDataResult(true, model); } @@ -497,6 +501,14 @@ await loginPageSettings.UpdateDb(option => await core.SetSdkSetting(Settings.httpServerAddress, adminSettingsModel.SiteLink); } + if (!string.IsNullOrEmpty(adminSettingsModel.ThemeVariant)) + { + await appearanceSettings.UpdateDb(option => + { + option.ThemeVariant = adminSettingsModel.ThemeVariant; + }, dbContext); + } + // if (adminSettingsModel.SwiftSettingsModel != null) // { // await core.SetSdkSetting( diff --git a/eFormAPI/eFormAPI.Web/Startup.cs b/eFormAPI/eFormAPI.Web/Startup.cs index 02197dabff..2231b5bfc9 100644 --- a/eFormAPI/eFormAPI.Web/Startup.cs +++ b/eFormAPI/eFormAPI.Web/Startup.cs @@ -241,6 +241,8 @@ public void ConfigureServices(IServiceCollection services) services.ConfigureDbOptions(Configuration.GetSection("EmailSettings")); services.ConfigureDbOptions(Configuration.GetSection("LoginPageSettings")); services.ConfigureDbOptions(Configuration.GetSection("HeaderSettings")); + services.ConfigureDbOptions( + Configuration.GetSection("AppearanceSettings")); var configurationSection = Configuration.GetSection("ConnectionStringsSdk"); if (Configuration.MyConnectionString().Contains("127.0.0.1")) { diff --git a/eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj b/eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj index 741e9abcbc..c305b59ad7 100644 --- a/eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj +++ b/eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj @@ -56,8 +56,8 @@ - - + + diff --git a/eform-client/src/app/common/models/cms/cms.models.ts b/eform-client/src/app/common/models/cms/cms.models.ts index 4987f32512..4a94a61ad3 100644 --- a/eform-client/src/app/common/models/cms/cms.models.ts +++ b/eform-client/src/app/common/models/cms/cms.models.ts @@ -1,6 +1,7 @@ export interface CmsPublicConfigModel { isCmsEnabled: boolean; isMenuSticky: boolean; + themeVariant?: 'eform' | 'workspace'; } export interface CmsMenuItemModel { diff --git a/eform-client/src/app/common/models/settings/admin/admin-settings.model.ts b/eform-client/src/app/common/models/settings/admin/admin-settings.model.ts index d9eb965e1c..3af3515e6e 100644 --- a/eform-client/src/app/common/models/settings/admin/admin-settings.model.ts +++ b/eform-client/src/app/common/models/settings/admin/admin-settings.model.ts @@ -18,6 +18,7 @@ export class AdminSettingsModel { sdkSettingsModel: SdkSettingsModel; siteLink: string; assemblyVersion: string; + themeVariant: 'eform' | 'workspace' = 'eform'; constructor() { this.loginPageSettingsModel = new LoginPageSettingsModel(); diff --git a/eform-client/src/app/common/models/settings/user-settings.model.ts b/eform-client/src/app/common/models/settings/user-settings.model.ts index bf274ee122..bf1569a751 100644 --- a/eform-client/src/app/common/models/settings/user-settings.model.ts +++ b/eform-client/src/app/common/models/settings/user-settings.model.ts @@ -3,6 +3,7 @@ export class UserSettingsModel { formats: string; timeZone: string; darkTheme: boolean; + themeVariant: string; loginRedirectUrl: string; languageId: number; profilePicture: string; diff --git a/eform-client/src/app/common/models/user/user-info-model.ts b/eform-client/src/app/common/models/user/user-info-model.ts index 54b1f9db46..4c2eca19b2 100644 --- a/eform-client/src/app/common/models/user/user-info-model.ts +++ b/eform-client/src/app/common/models/user/user-info-model.ts @@ -8,6 +8,7 @@ export class UserInfoModel { fullName: string; role: string; darkTheme: boolean; + themeVariant?: string; isDeviceUser: boolean; language: string; timeZone: string; @@ -36,6 +37,7 @@ export class UserInfoModel { this.fullName = data.fullName; this.role = data.role; this.darkTheme = data.darkTheme; + this.themeVariant = data.themeVariant; this.isDeviceUser = data.isDeviceUser; this.language = data.language; this.timeZone = data.timeZone; diff --git a/eform-client/src/app/components/app.component.ts b/eform-client/src/app/components/app.component.ts index 80c7f2022d..3f6942eafb 100644 --- a/eform-client/src/app/components/app.component.ts +++ b/eform-client/src/app/components/app.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, Renderer2, inject } from '@angular/core'; import {Title} from '@angular/platform-browser'; import {AuthService, TitleService, UserSettingsService} from 'src/app/common/services'; import {Router} from '@angular/router'; @@ -9,6 +9,7 @@ import { AuthSyncStorageService, updateUserInfo, } from 'src/app/state'; +import {loadCmsConfig, selectCmsThemeVariant} from 'src/app/state/cms'; import {AuthStateService} from 'src/app/common/store'; import {TranslateService} from '@ngx-translate/core'; @@ -27,11 +28,17 @@ export class AppComponent implements OnInit, OnDestroy { private ngTitle = inject(Title); private titleService = inject(TitleService); private authSyncStorageService = inject(AuthSyncStorageService); + private renderer = inject(Renderer2); public selectIsAuth$ = this.authStore.select(selectAuthIsAuth); + private selectCmsThemeVariant$ = this.authStore.select(selectCmsThemeVariant); ngOnInit(): void { this.authSyncStorageService.init(); + this.authStore.dispatch(loadCmsConfig()); + this.selectCmsThemeVariant$.subscribe((variant) => { + this.applyThemeVariant(variant ?? 'eform'); + }); this.selectIsAuth$.pipe(debounceTime(1000), take(1)).subscribe((isAuth) => { if (isAuth) { zip(this.userSettings.getUserSettings(), this.service.obtainUserClaims()) @@ -70,6 +77,13 @@ export class AppComponent implements OnInit, OnDestroy { }); } + private applyThemeVariant(variant: 'eform' | 'workspace') { + const body = document.body; + this.renderer.removeClass(body, 'theme-eform'); + this.renderer.removeClass(body, 'theme-workspace'); + this.renderer.addClass(body, `theme-${variant}`); + } + ngOnDestroy(): void { } } diff --git a/eform-client/src/app/components/layouts/full-layout/full-layout.component.html b/eform-client/src/app/components/layouts/full-layout/full-layout.component.html index ba0db6c9f0..2b6b19e1f0 100644 --- a/eform-client/src/app/components/layouts/full-layout/full-layout.component.html +++ b/eform-client/src/app/components/layouts/full-layout/full-layout.component.html @@ -41,7 +41,7 @@ -
+
diff --git a/eform-client/src/app/components/layouts/full-layout/full-layout.component.scss b/eform-client/src/app/components/layouts/full-layout/full-layout.component.scss index 12e21c6649..630f15eec3 100644 --- a/eform-client/src/app/components/layouts/full-layout/full-layout.component.scss +++ b/eform-client/src/app/components/layouts/full-layout/full-layout.component.scss @@ -34,3 +34,11 @@ align-items: center; gap: 10px; } + +.content-card { + background-color: var(--content-card-bg); + border-radius: var(--content-card-radius); + margin: var(--content-card-margin); + min-height: calc(100% - 2 * var(--content-card-margin, 0px)); + overflow: auto; +} diff --git a/eform-client/src/app/components/navigation/navigation.component.html b/eform-client/src/app/components/navigation/navigation.component.html index 6e879f6826..401678389a 100644 --- a/eform-client/src/app/components/navigation/navigation.component.html +++ b/eform-client/src/app/components/navigation/navigation.component.html @@ -11,6 +11,7 @@ [class.selected-node]="router.url === node.link" *ngIf="node.isInternalLink" routerLink="{{ node.link }}" + routerLinkActive="selected-node" class="nav-link px-3" [id]="node.e2EId" (click)="onClickOnNode()" diff --git a/eform-client/src/app/components/navigation/navigation.component.scss b/eform-client/src/app/components/navigation/navigation.component.scss index 735da8834d..2f3fd73dc3 100644 --- a/eform-client/src/app/components/navigation/navigation.component.scss +++ b/eform-client/src/app/components/navigation/navigation.component.scss @@ -7,29 +7,44 @@ mat-tree { mat-tree-node { display: inline-flex; + margin: 4px 0; .nav-link { width: 100%; text-align: inherit; - min-height: 48px; + min-height: var(--nav-item-height, 48px); + padding: 0 16px !important; display: flex; align-items: center; justify-content: start; + border-radius: var(--nav-item-radius, var(--rounded-full)) !important; + font-family: var(--brand-font-family, var(--font-family)) !important; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.1px; + + .mat-icon { + width: 20px; + height: 20px; + font-size: 20px; + line-height: 20px; + margin-right: 12px; + } span { white-space: initial; - line-height: 1.4em; + line-height: 1.3em; } &:hover { border-radius: var(--rounded-full) !important; - background-color: var(--mdc-theme-primary-dark, #00695C) !important; - color: var(--mdc-theme-on-primary) !important; + background-color: var(--hover-surface) !important; + color: var(--text-header) !important; .mat-icon{ - color: var(--mdc-theme-on-primary) !important; + color: var(--text-header) !important; } svg{ - fill: var(--mdc-theme-on-primary) !important; + fill: var(--text-header) !important; } } @@ -42,20 +57,29 @@ mat-tree { .selected-node { - background-color: var(--mdc-theme-primary) !important; - color: var(--mdc-theme-on-primary) !important; + background-color: var(--nav-selected-surface, #e8eaed) !important; + color: var(--nav-selected-text, var(--on-primary)) !important; border-radius: var(--rounded-full); + font-weight: 500; + + .mat-icon { + color: var(--nav-selected-text, var(--on-primary)) !important; + } + + svg { + fill: var(--nav-selected-text, var(--on-primary)) !important; + } &:hover { - background-color: var(--mdc-theme-primary-dark) !important; - color: var(--mdc-theme-on-primary) !important; + background-color: var(--nav-selected-surface, #e8eaed) !important; + color: var(--nav-selected-text, var(--on-primary)) !important; .mat-icon { - color: var(--mdc-theme-on-primary) !important; + color: var(--nav-selected-text, var(--on-primary)) !important; } svg { - fill: var(--mdc-theme-on-primary) !important; + fill: var(--nav-selected-text, var(--on-primary)) !important; } } } @@ -63,46 +87,70 @@ mat-tree { } .mat-nested-tree-node { // styles for nested tree nodes + display: block; + margin: 0 !important; + padding: 8px 0; + mat-accordion { mat-expansion-panel { + background: transparent !important; + display: block; + margin: 0 !important; + &:not([class*=mat-elevation-z]) { box-shadow: none !important; } .mat-expansion-panel-header { - padding-left: 1rem; + height: var(--nav-item-height, 48px) !important; + min-height: var(--nav-item-height, 48px) !important; + padding: 0 16px 0 12px !important; + border-radius: var(--nav-item-radius, var(--rounded-full)) !important; + font-family: var(--brand-font-family, var(--font-family)) !important; + font-size: 14px !important; + font-weight: 500 !important; + color: var(--text-header); + + .mat-expansion-panel-header-title, + .mat-content, + span, + a { + font-weight: 500 !important; + font-size: 14px !important; + } + + .mat-content { + align-items: center; + } + + .mat-expansion-indicator svg { + fill: var(--icon-secondary); + } &:hover { - background-color: var(--mdc-theme-primary-dark) !important; - color: var(--mdc-theme-on-primary) !important; - border-radius: var(--rounded-full); - - .mat-expansion-indicator{ - background: red!important; - svg{ - fill: red!important; - } - } + background-color: var(--hover-surface) !important; + color: var(--text-header) !important; - a { - span { - color: var(--mdc-theme-on-primary) !important; - } + .mat-icon { + color: var(--text-header) !important; } - .mat-icon{ - color: var(--mdc-theme-on-primary) !important; + + .mat-expansion-indicator svg { + fill: var(--text-header) !important; } } } .mat-expansion-panel-content { .mat-expansion-panel-body { + padding: 0 0 0 8px !important; + mat-tree-node { display: inline-flex; span { white-space: initial; - line-height: 1.4em; + line-height: 1.3em; } } } @@ -110,11 +158,18 @@ mat-tree { mat-tree-node { .nav-link { + min-height: 36px; + font-size: 14px; + &:hover { - width: calc(100% - 20px) !important; + width: 100% !important; border-radius: var(--rounded-full) !important; - background-color: var(--mdc-theme-primary-dark, #00695C) !important; - color: var(--mdc-theme-on-primary) !important; + background-color: var(--hover-surface) !important; + color: var(--text-header) !important; + + .mat-icon { + color: var(--text-header) !important; + } } } @@ -126,7 +181,7 @@ mat-tree { } .selected-node { - width: calc(100% - 20px) !important; + width: 100% !important; } } diff --git a/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.html b/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.html index 26d20dafdd..ca29b36689 100644 --- a/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.html +++ b/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.html @@ -292,5 +292,52 @@

{{ 'Languages' | translate }}

+ +
+ + +

+ {{ 'Appearance' | translate }} +

+
+ +

{{ 'Theme variant' | translate }}

+
+ + +
+
+
+
diff --git a/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.scss b/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.scss index 0bce7a688c..3633b12799 100644 --- a/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.scss +++ b/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.scss @@ -1,3 +1,77 @@ +.theme-picker-label { + margin: 0 0 12px; + font-weight: 500; + color: var(--text-body); +} + +.theme-picker { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.theme-option { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px; + border-radius: 12px; + border: 2px solid var(--border); + background: var(--card); + cursor: pointer; + min-width: 160px; + transition: border-color 0.15s ease, background 0.15s ease; + + input[type='radio'] { + position: absolute; + opacity: 0; + pointer-events: none; + } + + &:hover { + background: var(--hover-surface, var(--table-row-hover)); + } + + &--selected { + border-color: var(--primary); + background: var(--selected-surface, var(--card)); + } +} + +.theme-swatch { + display: flex; + width: 120px; + height: 72px; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + + .swatch-primary { + flex: 0 0 40%; + } + + .swatch-surface { + flex: 1; + } + + &--eform { + .swatch-primary { background: #009688; } + .swatch-surface { background: #fafafa; } + } + + &--workspace { + .swatch-primary { background: #0b57d0; } + .swatch-surface { background: #f0f4f9; } + } +} + +.theme-option-label { + font-size: 14px; + font-weight: 500; + color: var(--text-header); +} + .input-invisible { visibility: hidden; width: 0; diff --git a/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.ts b/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.ts index 2adcbca656..4a1200e916 100644 --- a/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.ts +++ b/eform-client/src/app/modules/application-settings/components/admin-settings/admin-settings.component.ts @@ -8,6 +8,7 @@ import {AuthStateService} from 'src/app/common/store'; import {AppSettingsStateService} from 'src/app/modules/application-settings/components/store'; import * as R from 'ramda'; import {selectAuthIsAuth, selectBearerToken, selectCurrentUserIsAdmin} from 'src/app/state/auth/auth.selector'; +import {loadCmsConfig} from 'src/app/state/cms'; import {Store} from '@ngrx/store'; import {zip} from 'rxjs'; import {tap} from 'rxjs/operators'; @@ -201,6 +202,7 @@ export class AdminSettingsComponent implements OnInit, AfterViewInit { this.headerImageUploader.clearQueue(); this.loginPageImageUploader.clearQueue(); this.eventBrokerService.emit('get-header-settings', null); + this.authStore.dispatch(loadCmsConfig()); } }); } diff --git a/eform-client/src/app/state/auth/auth.actions.ts b/eform-client/src/app/state/auth/auth.actions.ts index 2cf25f6adf..2fc1408e6b 100644 --- a/eform-client/src/app/state/auth/auth.actions.ts +++ b/eform-client/src/app/state/auth/auth.actions.ts @@ -31,6 +31,11 @@ export const updateDarkTheme = createAction( (payload: boolean) => ({payload}) ); +export const updateThemeVariant = createAction( + '[Auth] Update Theme Variant', + (payload: string) => ({payload}) +); + export const updateUserInfo = createAction( '[Auth] Update User Info', (payload: { userSettings: OperationDataResult, userClaims: UserClaimsModel }) => ({payload}) diff --git a/eform-client/src/app/state/auth/auth.recuder.ts b/eform-client/src/app/state/auth/auth.recuder.ts index 1891ed76d4..825832b844 100644 --- a/eform-client/src/app/state/auth/auth.recuder.ts +++ b/eform-client/src/app/state/auth/auth.recuder.ts @@ -7,6 +7,7 @@ import { loadAuthSuccess, logout, refreshToken, updateCurrentUserLocaleAndDarkTheme, updateDarkTheme, updateSideMenuOpened, + updateThemeVariant, updateUserInfo, updateUserLocale } from './'; import {StoreStatusEnum} from 'src/app/common/const'; @@ -21,6 +22,7 @@ export interface AuthCurrentUser { locale?: string; languageId?: number; darkTheme?: boolean; + themeVariant?: string; loginRedirectUrl?: string; claims?: UserClaimsModel; isFirstUser: boolean, @@ -61,6 +63,7 @@ export const authInitialState: AuthState = { locale: 'da', // TODO add env for test run languageId: 0, darkTheme: false, + themeVariant: 'eform', loginRedirectUrl: '', isFirstUser: false, avatarUrl: '', @@ -158,6 +161,7 @@ const _authReducer = createReducer( currentUser: { ...state.currentUser, darkTheme: payload.userSettings.model.darkTheme, + themeVariant: payload.userSettings.model.themeVariant || 'eform', locale: payload.userSettings.model.locale, loginRedirectUrl: payload.userSettings.model.loginRedirectUrl || '', claims: payload.userClaims, @@ -209,6 +213,13 @@ const _authReducer = createReducer( darkTheme: payload } })), + on(updateThemeVariant, (state, {payload}) => ({ + ...state, + currentUser: { + ...state.currentUser, + themeVariant: payload + } + })), ); export function authReducer(state: AuthState | undefined, action: any) { diff --git a/eform-client/src/app/state/auth/auth.selector.ts b/eform-client/src/app/state/auth/auth.selector.ts index 54e4798e0f..2abc6d9375 100644 --- a/eform-client/src/app/state/auth/auth.selector.ts +++ b/eform-client/src/app/state/auth/auth.selector.ts @@ -32,6 +32,8 @@ export const selectSideMenuOpened = createSelector(selectAuth, (state: AuthState) => state.sideMenuOpened); export const selectIsDarkMode = createSelector(selectAuthUser, (state) => state.darkTheme); +export const selectThemeVariant + = createSelector(selectAuthUser, (state) => state.themeVariant || 'eform'); export const selectCurrentUserLocale = createSelector(selectAuthUser, (state) => state.locale); export const selectCurrentUserLanguageId diff --git a/eform-client/src/app/state/cms/cms.actions.ts b/eform-client/src/app/state/cms/cms.actions.ts index 00a06a7bd0..efb299b339 100644 --- a/eform-client/src/app/state/cms/cms.actions.ts +++ b/eform-client/src/app/state/cms/cms.actions.ts @@ -5,7 +5,7 @@ export const loadCmsConfig = createAction('[Cms] Load Config'); export const loadCmsConfigSuccess = createAction( '[Cms] Load Config Success', - (payload: {isCmsEnabled: boolean; isMenuSticky: boolean}) => ({payload}) + (payload: {isCmsEnabled: boolean; isMenuSticky: boolean; themeVariant?: 'eform' | 'workspace'}) => ({payload}) ); export const loadCmsConfigFailure = createAction( diff --git a/eform-client/src/app/state/cms/cms.effects.ts b/eform-client/src/app/state/cms/cms.effects.ts index 400ecb4dc5..8797e850cc 100644 --- a/eform-client/src/app/state/cms/cms.effects.ts +++ b/eform-client/src/app/state/cms/cms.effects.ts @@ -24,7 +24,7 @@ export class CmsEffects { this.cmsService.getPublicConfig().pipe( map((result) => result.success - ? loadCmsConfigSuccess({isCmsEnabled: result.model.isCmsEnabled, isMenuSticky: result.model.isMenuSticky}) + ? loadCmsConfigSuccess({isCmsEnabled: result.model.isCmsEnabled, isMenuSticky: result.model.isMenuSticky, themeVariant: result.model.themeVariant}) : loadCmsConfigFailure({error: result.message}) ), catchError((err) => of(loadCmsConfigFailure({error: err.message}))) diff --git a/eform-client/src/app/state/cms/cms.reducer.ts b/eform-client/src/app/state/cms/cms.reducer.ts index 2957ce24cf..5a304b0884 100644 --- a/eform-client/src/app/state/cms/cms.reducer.ts +++ b/eform-client/src/app/state/cms/cms.reducer.ts @@ -14,6 +14,7 @@ export const CMS_REDUCER_NODE = 'cms'; export interface CmsState { isCmsEnabled: boolean; isMenuSticky: boolean; + themeVariant: 'eform' | 'workspace'; landingPage: CmsPublicLandingModel | null; isLoading: boolean; isLoaded: boolean; @@ -22,6 +23,7 @@ export interface CmsState { export const cmsInitialState: CmsState = { isCmsEnabled: false, isMenuSticky: false, + themeVariant: 'eform', landingPage: null, isLoading: false, isLoaded: false, @@ -34,6 +36,7 @@ const _cmsReducer = createReducer( ...state, isCmsEnabled: payload.isCmsEnabled, isMenuSticky: payload.isMenuSticky, + themeVariant: payload.themeVariant ?? 'eform', isLoading: false, isLoaded: true, })), diff --git a/eform-client/src/app/state/cms/cms.selectors.ts b/eform-client/src/app/state/cms/cms.selectors.ts index ae98e885da..d3b139d675 100644 --- a/eform-client/src/app/state/cms/cms.selectors.ts +++ b/eform-client/src/app/state/cms/cms.selectors.ts @@ -5,6 +5,7 @@ export const selectCmsState = (state: AppState) => state.cms; export const selectIsCmsEnabled = createSelector(selectCmsState, (state) => state.isCmsEnabled); export const selectIsMenuSticky = createSelector(selectCmsState, (state) => state.isMenuSticky); +export const selectCmsThemeVariant = createSelector(selectCmsState, (state) => state.themeVariant); export const selectCmsLandingPage = createSelector(selectCmsState, (state) => state.landingPage); export const selectCmsIsLoading = createSelector(selectCmsState, (state) => state.isLoading); export const selectCmsIsLoaded = createSelector(selectCmsState, (state) => state.isLoaded); diff --git a/eform-client/src/index.html b/eform-client/src/index.html index 468aa38001..f41c25a858 100644 --- a/eform-client/src/index.html +++ b/eform-client/src/index.html @@ -16,6 +16,8 @@ + + diff --git a/eform-client/src/scss/components/_chart.scss b/eform-client/src/scss/components/_chart.scss index 15a2e913f5..eeb275f7dc 100644 --- a/eform-client/src/scss/components/_chart.scss +++ b/eform-client/src/scss/components/_chart.scss @@ -1,184 +1,172 @@ -@use 'sass:map'; -@use '@angular/material' as mat; - -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - $is-dark-theme: map.get($color-config, 'is-dark'); - $background: map.get(map.get($theme, 'background'), 'card'); - - @if ($is-dark-theme) { - /*Backgrounds*/ - $color-bg-darkest: #13141b; - $color-bg-darker: #1b1e27; - $color-bg-dark: #232837; - $color-bg-med: #2f3646; - $color-bg-light: #455066; - $color-bg-lighter: #5b6882; - - /*Text*/ - $color-text-dark: #72809b; - $color-text-med-dark: #919db5; - $color-text-med: #A0AABE; - $color-text-med-light: #d9dce1; - $color-text-light: #f0f1f6; - $color-text-lighter: #fff; - - background: $color-bg-darker; - - .ngx-charts { - text { - fill: $color-text-med; - } +@mixin _dark-chart { + $color-bg-darker: #1b1e27; + $color-bg-dark: #232837; + $color-bg-med: #2f3646; + $color-bg-light: #455066; - .tooltip-anchor { - fill: rgb(255, 255, 255); - } + $color-text-dark: #72809b; + $color-text-med: #A0AABE; + $color-text-light: #f0f1f6; - .gridline-path { - stroke: $color-bg-med; - } + background: $color-bg-darker; - .refline-path { - stroke: $color-bg-light; - } + .ngx-charts { + text { + fill: $color-text-med; + } - .reference-area { - fill: #fff; - } + .tooltip-anchor { + fill: rgb(255, 255, 255); + } - .grid-panel { - &.odd { - rect { - fill: rgba(255, 255, 255, 0.05); - } - } - } + .gridline-path { + stroke: $color-bg-med; + } - .force-directed-graph { - .edge { - stroke: $color-bg-light; - } - } + .refline-path { + stroke: $color-bg-light; + } - .number-card { - p { - color: $color-text-light; + .reference-area { + fill: #fff; + } + + .grid-panel { + &.odd { + rect { + fill: rgba(255, 255, 255, 0.05); } } + } - .gauge { - .background-arc { - path { - fill: $color-bg-med; - } - } + .force-directed-graph { + .edge { + stroke: $color-bg-light; + } + } - .gauge-tick { - path { - stroke: $color-text-med; - } + .number-card { + p { + color: $color-text-light; + } + } - text { - fill: $color-text-med; - } + .gauge { + .background-arc { + path { + fill: $color-bg-med; } } - .linear-gauge { - .background-bar { - path { - fill: $color-bg-med; - } + .gauge-tick { + path { + stroke: $color-text-med; } - .units { - fill: $color-text-dark; + text { + fill: $color-text-med; } } + } - .timeline { - .brush-background { - fill: rgba(255, 255, 255, 0.05); - } - - .brush { - .selection { - fill: rgba(255, 255, 255, 0.1); - stroke: #aaa; - } + .linear-gauge { + .background-bar { + path { + fill: $color-bg-med; } } - .polar-chart .polar-chart-background { - fill: rgb(30, 34, 46); + .units { + fill: $color-text-dark; } } - .chart-legend { - .legend-labels { - background: $background !important; + .timeline { + .brush-background { + fill: rgba(255, 255, 255, 0.05); } - .legend-item { - &:hover { - color: #fff; + .brush { + .selection { + fill: rgba(255, 255, 255, 0.1); + stroke: #aaa; } } + } - .legend-label { - &:hover { - color: #fff !important; - } + .polar-chart .polar-chart-background { + fill: rgb(30, 34, 46); + } + } - .active { - .legend-label-text { - color: #fff !important; - text-overflow: clip !important; - padding-right: 5px; - } - } - } + .chart-legend { + .legend-labels { + background: var(--card) !important; + } - .scale-legend-label { - color: $color-text-med; + .legend-item { + &:hover { + color: #fff; } } - .advanced-pie-legend { - color: $color-text-med; - - .legend-item { - &:hover { - color: #fff !important; - } + .legend-label { + &:hover { + color: #fff !important; } - .legend-items-container { - .legend-items { - overflow: visible !important; + .active { + .legend-label-text { + color: #fff !important; + text-overflow: clip !important; + padding-right: 5px; } } } - .number-card .number-card-label { - font-size: 0.8em; + .scale-legend-label { color: $color-text-med; } - } @else { // light theme - .advanced-pie-legend { - .legend-items-container { - .legend-items { - overflow: visible !important; - } + } + + .advanced-pie-legend { + color: $color-text-med; + + .legend-item { + &:hover { + color: #fff !important; } } + + .legend-items-container { + .legend-items { + overflow: visible !important; + } + } + } + + .number-card .number-card-label { + font-size: 0.8em; + color: $color-text-med; + } } + +@mixin _light-chart { + .advanced-pie-legend { + .legend-items-container { + .legend-items { + overflow: visible !important; + } + } + } } -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); +@mixin theme { + body.theme-light { + @include _light-chart; + } + body.theme-dark { + @include _dark-chart; } } diff --git a/eform-client/src/scss/components/_material-dropdown.scss b/eform-client/src/scss/components/_material-dropdown.scss index 579c8da3ca..d565d25630 100644 --- a/eform-client/src/scss/components/_material-dropdown.scss +++ b/eform-client/src/scss/components/_material-dropdown.scss @@ -567,7 +567,61 @@ body.theme-light { } */ body { - .ng-dropdown-panel{ + .ng-dropdown-panel { z-index: 100000; + background: var(--card) !important; + border: 1px solid var(--border) !important; + border-radius: var(--dropdown-panel-radius, 4px) !important; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px 0 rgba(0, 0, 0, 0.04) !important; + overflow: hidden; + + .ng-dropdown-panel-items { + padding: 8px 0; + + .ng-option { + background: transparent !important; + color: var(--text-header) !important; + font-family: var(--brand-font-family, var(--font-family)) !important; + font-size: 14px !important; + font-weight: 400 !important; + padding: 0 16px !important; + min-height: var(--dropdown-option-height, 48px) !important; + line-height: var(--dropdown-option-height, 48px) !important; + display: flex; + align-items: center; + + &.ng-option-marked, + &.ng-option-marked * { + background: var(--selected-surface) !important; + color: var(--on-primary) !important; + } + + &.ng-option-selected { + background: var(--selected-surface) !important; + color: var(--on-primary) !important; + font-weight: 500 !important; + + &.ng-option-marked { + background: var(--selected-surface) !important; + color: var(--on-primary) !important; + } + } + + &.ng-option-disabled { + opacity: 0.5; + } + } + + .ng-optgroup { + background: transparent !important; + color: var(--text-body) !important; + font-weight: 500 !important; + padding: 0 16px !important; + + &.ng-option-marked { + background: var(--table-row-hover) !important; + } + } + } } } diff --git a/eform-client/src/scss/components/_table.scss b/eform-client/src/scss/components/_table.scss index 399a53fac3..f5472f013a 100644 --- a/eform-client/src/scss/components/_table.scss +++ b/eform-client/src/scss/components/_table.scss @@ -1,6 +1,3 @@ -@use 'sass:map'; -@use '@angular/material' as mat; - .mtx-grid { max-height: 78.5vh; border: 0 !important; @@ -25,151 +22,45 @@ } } -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - $is-dark-theme: map.get($color-config, 'is-dark'); - $background-powder-color: #b3d3ea; - $background-yellow-color: #e6d178; - $background-red-light-color: #f8d7da; - $background-red-dark-color: #f5a5a8; - @if $is-dark-theme { - $background-powder-color: #4c6071; - $background-yellow-color: #7e6f3a; - $background-red-light-color: #f8d7da; - $background-red-dark-color: #f5a5a8; - } +@mixin _grid-backgrounds($powder, $yellow, $red-light, $red-dark) { mtx-grid { - .background-yellow { - background-color: $background-yellow-color !important; - color: #0F1316 !important; - - span { + @each $name, $bg in (('yellow', $yellow), ('red-light', $red-light), ('red-dark', $red-dark), ('powder', $powder)) { + .background-#{$name} { + background-color: $bg !important; color: #0F1316 !important; - } - td.mat-table-sticky-right { - background-color: $background-yellow-color !important; - - #actionMenu { - .mat-icon { - color: #0F1316 !important; - } + span { + color: #0F1316 !important; } - } - .mat-mdc-form-field { - padding: 4px !important; - } - - .mat-mdc-form-field .mdc-text-field__input { - background-color: unset !important; - } - } - - .background-red-light { - background-color: $background-red-light-color !important; - color: #0F1316 !important; + td.mat-table-sticky-right { + background-color: $bg !important; - span { - color: #0F1316 !important; - } - - td.mat-table-sticky-right { - background-color: $background-red-light-color !important; - - #actionMenu { - .mat-icon { - color: #0F1316 !important; + #actionMenu { + .mat-icon { + color: #0F1316 !important; + } } } - } - - .mat-mdc-form-field { - padding: 4px !important; - } - .mat-mdc-form-field .mdc-text-field__input { - background-color: unset !important; - } - } - - .background-red-dark { - background-color: $background-red-dark-color !important; - color: #0F1316 !important; - - span { - color: #0F1316 !important; - } - - td.mat-table-sticky-right { - background-color: $background-red-dark-color !important; - - #actionMenu { - .mat-icon { - color: #0F1316 !important; - } + .mat-mdc-form-field { + padding: 4px !important; } - } - - .mat-mdc-form-field { - padding: 4px !important; - } - - .mat-mdc-form-field .mdc-text-field__input { - background-color: unset !important; - } - } - - .background-powder { - background-color: $background-powder-color !important; - color: #0F1316 !important; - - span { - color: #0F1316 !important; - } - - td.mat-table-sticky-right { - background-color: $background-powder-color !important; - #actionMenu { - .mat-icon { - color: #0F1316 !important; - } + .mat-mdc-form-field .mdc-text-field__input { + background-color: unset !important; } - - } - - .mat-mdc-form-field { - padding: 4px !important; - } - - .mat-mdc-form-field .mdc-text-field__input { - background-color: unset !important; } } } } -@mixin typography($theme) { - $typography-config: mat.m2-get-typography-config($theme); - $header-text: map.get($typography-config, 'subtitle-2'); - $normal-text: map.get($typography-config, 'body-2'); - mtx-grid thead * { - @each $prop, $value in $header-text { - $prop: $value; - } +@mixin theme { + body.theme-light { + @include _grid-backgrounds(#b3d3ea, #e6d178, #f8d7da, #f5a5a8); } - mtx-grid tbody * { - @each $prop, $value in $normal-text { - $prop: $value; - } - } -} -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); - @include typography($theme); + body.theme-dark { + @include _grid-backgrounds(#4c6071, #7e6f3a, #f8d7da, #f5a5a8); } } diff --git a/eform-client/src/scss/components/_tag.scss b/eform-client/src/scss/components/_tag.scss index 19c0a33e6c..302ba3ae4c 100644 --- a/eform-client/src/scss/components/_tag.scss +++ b/eform-client/src/scss/components/_tag.scss @@ -1,6 +1,3 @@ -@use 'sass:map'; -@use 'sass:color'; -@use '@angular/material' as mat; @use "../utilities" as *; .tag { @@ -21,46 +18,16 @@ margin-bottom: 10px; } -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - $background: map.get($theme, 'background'); - $is-dark-theme: map.get($color-config, 'is-dark'); - $foreground: map.get($color-config, foreground); - $text-color: mat.m2-get-color-from-palette($foreground, text); - $icon-color: mat.m2-get-color-from-palette($foreground, icon); - $disabled-button: mat.m2-get-color-from-palette($background, disabled-button); - - $background-color: color.scale(map.get($background, 'card'), $lightness: +15%); - @if $is-dark-theme { - $background-color: color.scale(map.get($background, 'card'), $lightness: -15%); - } - +@mixin theme { .tag { - background-color: $background-color; - color: $text-color; - border-color: $disabled-button; + background-color: var(--card); + color: var(--text-header); + border-color: var(--border); border-width: .5px; border-style: solid; + mat-icon { - color: $icon-color; - } - } -} -@mixin typography($theme) { - $typography-config: mat.m2-get-typography-config($theme); - $header-text: map.get($typography-config, 'subtitle-2'); - $normal-text: map.get($typography-config, 'body-2'); - .tag * { - @each $prop, $value in $normal-text { - $prop: $value; + color: var(--text-header); } } } - -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); - @include typography($theme); - } -} diff --git a/eform-client/src/scss/components/_text.scss b/eform-client/src/scss/components/_text.scss index 8e9c19f043..f2b1ca0516 100644 --- a/eform-client/src/scss/components/_text.scss +++ b/eform-client/src/scss/components/_text.scss @@ -1,10 +1,8 @@ -@use 'sass:map'; -@use '@angular/material' as mat; @use '../utilities/colors' as *; + body, .theme-light { --error: #{$error-light}; --warning: #{$warning-light}; - } body, .theme-dark { @@ -22,11 +20,11 @@ a { } .text-danger { - color: var(--error) ; + color: var(--error); } .text-warning { - color: var(--warning) ; + color: var(--warning); } .text-black { @@ -36,28 +34,19 @@ a { .text-white { color: white; } + @each $key in (nowrap, unset, break-spaces, normal, pre, pre-line, pre-wrap) { .text-#{$key} { white-space: $key; } } -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - $warn-palette: map.get($color-config, 'warn'); - $accent-palette: map.get($color-config, 'accent'); - $primary-palette: map.get($color-config, 'primary'); - - @each $key, $val in (('warn', $warn-palette), ('accent', $accent-palette), ('primary', $primary-palette)) { - .text-#{$key} { - color: mat.m2-get-color-from-palette($val, 500); - } +@mixin theme { + .text-primary, + .text-accent { + color: var(--primary); } -} - -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); + .text-warn { + color: var(--error); } } diff --git a/eform-client/src/scss/libs/ngx-editor/_ngx-editor.scss b/eform-client/src/scss/libs/ngx-editor/_ngx-editor.scss index 7cdece5ae8..d648ffff73 100644 --- a/eform-client/src/scss/libs/ngx-editor/_ngx-editor.scss +++ b/eform-client/src/scss/libs/ngx-editor/_ngx-editor.scss @@ -1,40 +1,29 @@ -@use 'sass:map'; -@use 'sass:color'; -@use '@angular/material' as mat; - -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - $is-dark-theme: map.get($color-config, 'is-dark'); - - $foreground: map.get($color-config, foreground); - $label-color: mat.m2-get-color-from-palette($foreground, secondary-text); - - $primary-palette: map.get($color-config, 'primary'); - $background: map.get($theme, 'background'); - +@mixin theme { .NgxEditor__MenuBar { - background-color: color.scale(map.get($background, 'card'), $lightness: +30%); + background-color: var(--input-fill); + border-radius: 4px 4px 0 0; } .NgxEditor { - color: if($is-dark-theme, white, black); - background-color: map.get($background, 'card'); - border-bottom: 1px solid mat.m2-get-color-from-palette($primary-palette, 50) !important; + background-color: var(--input-fill); + border-bottom: 1px solid var(--border) !important; &:focus-within:not([readonly]) { - box-shadow: 0 1px 0 0 mat.m2-get-color-from-palette($primary-palette, 500) !important; - border-bottom: 1px solid mat.m2-get-color-from-palette($primary-palette, 500) !important; + box-shadow: 0 1px 0 0 var(--input-focus-underline) !important; + border-bottom: 2px solid var(--input-focus-underline) !important; } + .NgxEditor__Placeholder:before { - color: $label-color; + color: var(--text-body); } } -} -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); + body.theme-light .NgxEditor { + color: black; + } + + body.theme-dark .NgxEditor { + color: white; } } diff --git a/eform-client/src/scss/libs/ngx-gallery/ngx-gallery.scss b/eform-client/src/scss/libs/ngx-gallery/ngx-gallery.scss index 7580349f0f..7847300374 100644 --- a/eform-client/src/scss/libs/ngx-gallery/ngx-gallery.scss +++ b/eform-client/src/scss/libs/ngx-gallery/ngx-gallery.scss @@ -1,26 +1,9 @@ -@use 'sass:map'; -@use '@angular/material' as mat; @use "lib/gallery"; @use "lib/lightbox"; -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - $background: map.get($theme, 'background'); - +@mixin theme { gallery { - background: map.get($background, 'card') !important; - } - - .g-backdrop { - //background-color: map.get($background, 'card') !important; - } - -} - -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); + background: var(--card) !important; } } diff --git a/eform-client/src/scss/libs/ngx-toastr/_ngx-toastr.scss b/eform-client/src/scss/libs/ngx-toastr/_ngx-toastr.scss index 95a815209b..5c0cd2afe6 100644 --- a/eform-client/src/scss/libs/ngx-toastr/_ngx-toastr.scss +++ b/eform-client/src/scss/libs/ngx-toastr/_ngx-toastr.scss @@ -1,24 +1,10 @@ -@use 'sass:map'; -@use '@angular/material' as mat; - -@mixin color($theme) { - $color-config: mat.m2-get-color-config($theme); - - $warn-palette: map.get($color-config, warn); - $accent-palette: map.get($color-config, accent); - $primary-palette: map.get($color-config, primary); - $background: map.get($theme, 'background'); - - @each $key, $val in (('warn', $warn-palette), ('accent', $accent-palette), ('primary', $primary-palette)) { - .ngx-toastr-#{$key} { - background-color: mat.m2-get-color-from-palette($val, 500); - } +@mixin theme { + .ngx-toastr-primary, + .ngx-toastr-accent { + background-color: var(--primary); } -} -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); + .ngx-toastr-warn { + background-color: var(--error); } } diff --git a/eform-client/src/scss/libs/theme.scss b/eform-client/src/scss/libs/theme.scss index 987561aa29..f4de8a62f2 100644 --- a/eform-client/src/scss/libs/theme.scss +++ b/eform-client/src/scss/libs/theme.scss @@ -1,257 +1,103 @@ /* You can add global styles to this file, and also import other style files 2 */ @use '@angular/material' as mat; -@use '@ng-matero/extensions' as mtx; +@use '../themes/eform' as eform; +@use '../themes/workspace' as workspace; +@use '../themes/theme-mixin' as theme-mixin; @use './ngx-editor/ngx-editor' as ngx-editor; @use '../components/text' as text; @use '../libs/ngx-gallery/ngx-gallery' as ngx-gallery; @use '../components/table' as table; @use '../components/chart' as chart; @use '../libs/ngx-toastr/ngx-toastr' as ngx-toastr; -//@use '../libs/ng-datepicker/ng-datepicker' as ng-datepicker; @use '../components/tag' as tag; @use '../utilities/colors' as *; -@include mat.core(); - -$letter-spacing: normal; -$eform-font-family: "Nunito Sans", Roboto, "Helvetica Neue", sans-serif; - - -// Define a typography -$eform-typography: mat.m2-define-typography-config( - $font-family: $eform-font-family, - $headline-4: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 400, - $font-size: 1.3rem, - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $headline-5: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 1.2rem, - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $headline-6: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 1.17rem, - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $subtitle-1: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 1.15rem, - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $subtitle-2: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 12.8px, // font size for: table header - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $body-1: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 16px, // font size for: default level text, text in table - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $body-2: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 1rem, - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $caption: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 1rem, - $line-height: 1.2, - $letter-spacing: $letter-spacing, - ), - $button: mat.m2-define-typography-level( - $font-family: $eform-font-family, - $font-weight: 300, - $font-size: 14px, // font size for: button text, button icon(?) - $line-height: 1.15, - $letter-spacing: $letter-spacing, - )); +@include mat.core(); -// Define a light theme -//$eform-light-primary: mat.m2-define-palette(mat.$m2-indigo-palette); -$eform-light-primary: mat.m2-define-palette(mat.$m2-teal-palette); -$eform-light-accent: mat.m2-define-palette(mat.$m2-teal-palette); -$eform-light-theme: mat.m2-define-light-theme(( - color: ( - primary: $eform-light-primary, - accent: $eform-light-accent, - ), - typography: $eform-typography, -)); +body { + &.theme-light { + @include theme-mixin.apply-theme(eform.$eform-tokens, light); + } + &.theme-dark { + @include theme-mixin.apply-theme(eform.$eform-tokens, dark); + } -// Define a dark theme -//$eform-dark-primary: mat.m2-define-palette(mat.$m2-blue-grey-palette); -$eform-dark-primary: mat.m2-define-palette(mat.$m2-teal-palette); -$eform-dark-accent: mat.m2-define-palette(mat.$m2-teal-palette); -$eform-dark-theme: mat.m2-define-dark-theme(( - color: ( - primary: $eform-dark-primary, - accent: $eform-dark-accent, - ), - typography: $eform-typography, -)); + &.theme-eform.theme-light { + @include theme-mixin.apply-theme(eform.$eform-tokens, light); + } + &.theme-eform.theme-dark { + @include theme-mixin.apply-theme(eform.$eform-tokens, dark); + } -$eform-m3-light-theme: mat.define-theme(( - color: ( - theme-type: light, - primary: mat.$cyan-palette - ), - typography: ( - brand-family: $eform-font-family, - plain-family: $eform-font-family - ), - density: ( - scale: 0, - ), -)); + &.theme-workspace.theme-light { + @include theme-mixin.apply-theme(workspace.$workspace-tokens, light); + } -$eform-m3-dark-theme: mat.define-theme(( - color: ( - theme-type: dark, - primary: mat.$cyan-palette, - ), - typography: ( - brand-family: $eform-font-family, - plain-family: $eform-font-family - ), - density: ( - scale: 0, - ), -)); + &.theme-workspace.theme-dark { + @include theme-mixin.apply-theme(workspace.$workspace-tokens, dark); + } + @include ngx-editor.theme; + @include text.theme; + @include ngx-gallery.theme; + @include table.theme; + @include ngx-toastr.theme; + @include chart.theme; + @include tag.theme; -body { - @each $key, $val in (('light', $eform-light-theme), ('dark', $eform-dark-theme)) { - &.theme-#{$key} { - //@include mat.all-component-themes($val); - @include mat.all-component-themes( - if($key == 'light', $eform-m3-light-theme, $eform-m3-dark-theme) - ); - @include ngx-editor.theme($val); - @include text.theme($val); - @include ngx-gallery.theme($val); - @include table.theme($val); - @include ngx-toastr.theme($val); - @include chart.theme($val); - //@include ng-datepicker.theme($val); - @include tag.theme($val); - //@include mtx.all-experimental-component-themes($val); - } - } &.mat-typography { margin: 0 0 0 0; } + #main-header-text { font-weight: 400; } } + .mdc-evolution-chip__action { padding-top: 3px !important; padding-bottom: 0px !important; } + .mdc-evolution-chip__text-label > span { position: relative; top: -1px; } + .mat-mdc-icon-button { - padding: 0 !important; - width: 28px !important; - height: 28px !important; + padding: 12px !important; + width: var(--icon-button-size, 48px) !important; + height: var(--icon-button-size, 48px) !important; + --mdc-icon-button-state-layer-size: var(--icon-button-size, 48px) !important; } + .mat-calendar-previous-button.mdc-icon-button.mat-mdc-icon-button.mat-unthemed.mat-mdc-button-base { --mdc-icon-button-state-layer-size: 40px !important; width: var(--mdc-icon-button-state-layer-size) !important; height: var(--mdc-icon-button-state-layer-size) !important; padding: 8px !important; } + .mat-calendar-next-button.mdc-icon-button.mat-mdc-icon-button.mat-unthemed.mat-mdc-button-base { --mdc-icon-button-state-layer-size: 40px !important; width: var(--mdc-icon-button-state-layer-size) !important; height: var(--mdc-icon-button-state-layer-size) !important; padding: 8px !important; } -// padding: 0px;// !important; -// height: 28px;// !important; -// width: 28px;// !important; -//} -// -.mat-calendar-previous-button.mdc-icon-button.mat-mdc-icon-button.mat-unthemed.mat-mdc-button-base { - --mdc-icon-button-state-layer-size: 40px; - width: var(--mdc-icon-button-state-layer-size); - height: var(--mdc-icon-button-state-layer-size); - padding: 8px !important; -} + .microting-uid { - color: mat.m2-get-color-from-palette(mat.$m2-indigo-palette, 300); + color: #7986cb; } + .mat-sort-header-content { text-align: start !important; } + .theme-dark { .mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label { - color: white; // Replace with your desired color value + color: white; } } -.cdk-overlay-container { - @include mat.dialog-overrides(( - container-shape: 20px, - container-elevation-shadow: none, - actions-padding: 20px, - )); -} -/* Global button shape overrides */ -button.mat-mdc-raised-button, -a.mat-mdc-raised-button, -button.mat-mdc-unelevated-button, -a.mat-mdc-unelevated-button, -button.mat-mdc-outlined-button, -a.mat-mdc-outlined-button, -button.mat-mdc-text-button, -a.mat-mdc-text-button { - border-radius: 20px; -} -/* Icon buttons */ -button.mat-mdc-icon-button, -a.mat-mdc-icon-button { - border-radius: 20px; -} -/* Optional: FABs */ -button.mat-mdc-fab, -a.mat-mdc-fab, -button.mat-mdc-mini-fab, -a.mat-mdc-mini-fab { - border-radius: 20px; -} -body, .theme-light { - --mdc-theme-primary: #{$primary}; - --mdc-theme-on-primary: #{$on-primary}; - --mdc-theme-primary-dark: #{$primary-dark-light-mode}; - --mdc-theme-on-primary-dark: #{$on-primary-dark}; - --mat-app-background-color: #{$bg-light} -} -body, .theme-dark { - --mdc-theme-primary: #{$primary-dark}; - --mdc-theme-on-primary: #{$on-primary-dark}; - --mdc-theme-primary-dark: #{$primary-dark-dark-mode}; - --mdc-theme-on-primary-dark: #{$on-primary-dark}; - --mat-app-background-color: #{$bg-dark} -} + diff --git a/eform-client/src/scss/styles.scss b/eform-client/src/scss/styles.scss index 2647c6f522..4decfd43c9 100644 --- a/eform-client/src/scss/styles.scss +++ b/eform-client/src/scss/styles.scss @@ -10,7 +10,7 @@ html, body { font-size: 14px; margin: 0; --theme-body-font-color: var(--black-800); - font-family: $eform-font-family; + font-family: var(--font-family, #{$eform-font-family}); .mat-mdc-option { font-size: unset !important; @@ -50,32 +50,17 @@ h2 { :root { --rounded-full: #{$border-rounded-full}; --font-family: #{$eform-font-family}; -} - - -body, .theme-light { --black-800: #242729; --blue-600: #0077cc; --theme-body-font-color: var(--black-800); +} + +body, .theme-light { --tp-td-bg: #ffffff; --tp-th-bg: #f7f9fa; --tp-border: #EBEFF2; --tp-text: #111827; --tp-white-text: #0F1316; - --border: #{$border-light}; - --icon-secondary: #{$icon-secondary-light}; - --bg: #{$bg-light}; - --text-header: #{$text-header-light}; - //--mat-card-elevated-container-color: #{$bg-light}!important; - --primary: #{$primary-light}; - --primary-light: #{$primary-light-light-mode}; - --warning: #{$warning-light}; - --error: #{$error-light}; - --btn-delete-text: #{$text-header-dark}; - --text-body: #{$text-body-light}; - --card: #{$card-light}; - --mat-table-row-item-outline-color: #{$border-light} !important; - --mat-card-elevated-container-color: #{$bg-light} !important; } body, .theme-dark { @@ -84,20 +69,6 @@ body, .theme-dark { --tp-border: #2B2B2B; --tp-text: #E6E6E6; --tp-white-text: white; - --border: #{$border-dark}; - --icon-secondary: #{$icon-secondary-dark}; - --bg: #{$bg-dark}; - --text-header: #{$text-header-dark}; - //--mat-card-elevated-container-color: #{$bg-dark}!important; - --primary: #{$primary-dark}; - --primary-light: #{$primary-light-dark-mode}; - --warning: #{$warning-dark}; - --error: #{$error-dark}; - --btn-delete-text: #{$text-header-light}; - --text-body: #{$text-body-dark}; - --card: #{$card-dark}; - --mat-table-row-item-outline-color: #{$border-dark} !important; - --mat-card-elevated-container-color: #{$bg-dark} !important; } a { @@ -538,10 +509,12 @@ ngx-material-timepicker-container { background-color: var(--mdc-theme-primary) !important; color: var(--mdc-theme-on-primary) !important; cursor: pointer; - height: 48px; + height: var(--primary-button-height, 48px); + padding: 0 28px; + font-size: 14px; - &:hover { - background-color: var(--mdc-theme-primary-dark, #00695C) !important; + &:hover:not(:disabled) { + background-color: var(--primary-hover) !important; } &:disabled, @@ -556,8 +529,8 @@ ngx-material-timepicker-container { &--icon-left { display: inline-flex; - height: 48px; - padding: var(--312-px, 12px) var(--520-px, 20px); + height: var(--primary-button-height, 48px); + padding: 0 24px; justify-content: center; align-items: center; gap: var(--156-px, 6px); @@ -590,7 +563,7 @@ ngx-material-timepicker-container { background: unset; cursor: pointer; color: var(--text-header) !important; - height: 48px; + height: 36px; border-radius: var(--rounded-full); &:disabled, @@ -620,7 +593,7 @@ ngx-material-timepicker-container { align-items: center; gap: var(--28-px, 8px); border: 1px solid var(--border); - height: 58px !important; + height: 36px !important; } } @@ -630,8 +603,8 @@ ngx-material-timepicker-container { .btn-cancel { display: flex; - height: 48px; - padding: var(--312-px, 12px) var(--832-px, 32px); + height: 36px; + padding: 0 24px; justify-content: center; align-items: center; gap: var(--156-px, 6px); @@ -660,8 +633,8 @@ ngx-material-timepicker-container { .btn-warning { display: flex; - height: 48px; - padding: var(--312-px, 12px) var(--832-px, 32px); + height: 36px; + padding: 0 24px; justify-content: center; align-items: center; gap: var(--156-px, 6px); @@ -688,8 +661,8 @@ ngx-material-timepicker-container { .btn-delete { display: flex; - height: 48px; - padding: var(--312-px, 12px) var(--832-px, 32px); + height: 36px; + padding: 0 24px; justify-content: center; align-items: center; gap: var(--156-px, 6px); @@ -716,8 +689,8 @@ ngx-material-timepicker-container { .warning-btn { display: flex; - height: 48px; - padding: var(--312-px, 12px) var(--832-px, 32px); + height: 36px; + padding: 0 24px; justify-content: center; align-items: center; gap: var(--156-px, 6px); @@ -744,7 +717,9 @@ ngx-material-timepicker-container { .eform-sub-header { - background: var(--bg) !important; + background: var(--card) !important; + border-radius: 12px !important; + padding: 16px 24px !important; display: flex; justify-content: space-between; align-items: center; @@ -776,19 +751,21 @@ ngx-material-timepicker-container { } body.theme-dark .NgxEditor, body.theme-light .NgxEditor { - background: var(--bg) !important; - //border-bottom: unset!important; + background: var(--input-fill) !important; border: none !important; - color: var(--tp-text) !important; + color: var(--text-header) !important; &__MenuBar { - background: var(--card) !important; + background: var(--input-fill) !important; + border-radius: 4px 4px 0 0 !important; } } formatting-text-editor { - border: 1px solid var(--border) !important; - border-radius: 4px !important; + border: none !important; + border-radius: 4px 4px 0 0 !important; + background: var(--input-fill) !important; + display: block; } .ng-dropdown-panel { @@ -805,19 +782,18 @@ formatting-text-editor { .mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label { - top: calc(48px / 2) !important; - + top: calc(40px / 2) !important; } .mat-mdc-form-field { .mat-mdc-form-field-icon-suffix, .mat-mdc-form-field-icon-prefix { - color: var(--tp-text); + color: var(--icon-secondary); } .mat-mdc-form-field-flex { align-items: center; - height: 48px; + height: 40px; } .mat-mdc-floating-label { @@ -829,40 +805,30 @@ formatting-text-editor { } .mat-mdc-form-field-infix { - min-height: 48px !important; + min-height: 40px !important; } .mdc-text-field { - height: 48px !important; - border: 1px solid var(--border); - border-radius: var(--rounded-full, 9999px) !important; - - background-color: var(--bg) !important; - - &__ripple { - ::before { - background-color: var(--bg) !important; - } - - ::after { - background-color: var(--bg) !important; - } - } + height: 40px !important; + border: none !important; + border-radius: 4px 4px 0 0 !important; + background-color: var(--input-fill) !important; &__input { - background-color: var(--bg) !important; + background-color: transparent !important; + color: var(--text-header) !important; } &--filled:not(.mdc-text-field--disabled) { - background-color: var(--bg) !important; + background-color: var(--input-fill) !important; .mdc-line-ripple { &::before { - border-bottom: unset !important; + border-bottom: 1px solid var(--border) !important; } &::after { - border-bottom-color: var(--bg) !important; + border-bottom: 2px solid var(--input-focus-underline) !important; } } @@ -887,36 +853,35 @@ formatting-text-editor { background: unset !important; } - .mdc-text-field { - border: 1px solid var(--border); - border-radius: 4px !important; - - background-color: var(--bg) !important; + .mat-mdc-form-field-subscript-wrapper { + background: transparent !important; + display: none !important; + } - &__ripple { - ::before { - background-color: var(--bg) !important; - } + &.mat-form-field-invalid .mat-mdc-form-field-subscript-wrapper { + display: block !important; + } - ::after { - background-color: var(--bg) !important; - } - } + .mdc-text-field { + border: none !important; + border-radius: 4px 4px 0 0 !important; + background-color: var(--input-fill) !important; &__input { - background-color: var(--bg) !important; + background-color: transparent !important; + color: var(--text-header) !important; } &--filled:not(.mdc-text-field--disabled) { - background-color: var(--bg) !important; + background-color: var(--input-fill) !important; .mdc-line-ripple { &::before { - border-bottom: unset !important; + border-bottom: 1px solid var(--border) !important; } &::after { - border-bottom-color: var(--bg) !important; + border-bottom: 2px solid var(--input-focus-underline) !important; } } @@ -982,34 +947,107 @@ formatting-text-editor { border-bottom-color: var(--border) !important; } +.mtx-grid .mat-mdc-header-cell, +.mtx-grid .mat-mdc-cell { + border-bottom: 1px solid var(--border) !important; +} + +[aria-sort="ascending"], +[aria-sort="descending"] { + .mat-sort-header-content > svg.mtx-grid-icon { + display: none !important; + } + + .mat-sort-header-arrow { + width: 24px !important; + height: 24px !important; + min-width: 24px !important; + background: var(--selected-surface) !important; + border-radius: 9999px !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + opacity: 1 !important; + margin: 0 0 0 6px !important; + padding: 0 !important; + overflow: hidden !important; + + > * { + margin: 0 !important; + padding: 0 !important; + } + + svg { + position: static !important; + inset: auto !important; + width: 16px !important; + height: 16px !important; + fill: var(--primary) !important; + display: block; + } + } + + .mat-sort-header-content { + color: var(--primary); + font-weight: 500; + } +} + + .mtx-grid-toolbar { border: unset !important; } +.mtx-grid .mat-mdc-row:hover, +.mat-mdc-row:hover { + background: var(--table-row-hover) !important; +} + +.mtx-grid .mat-mdc-row.selected, +.mtx-grid .mat-mdc-row[aria-selected="true"], +.mat-mdc-row.selected, +.mat-mdc-row[aria-selected="true"] { + background: var(--selected-surface) !important; +} + +.mat-mdc-unelevated-button:hover:not(:disabled), +.mat-mdc-raised-button:hover:not(:disabled) { + background-color: var(--primary-hover) !important; +} + +button, +.btn-primary, +.btn-secondary, +.btn-cancel { + font-family: var(--brand-font-family, var(--font-family)) !important; +} + .mtx-grid { border-color: var(--border) !important; border: unset !important; outline: unset !important; .mat-mdc-table { - background: var(--bg) !important; + background: var(--card) !important; border-bottom: 1px solid var(--border) !important; border-color: var(--border) !important; .mat-mdc-header-row, thead { border-color: var(--border) !important; - background: var(--card) !important; + background: var(--table-header-bg, var(--card)) !important; tr { border-color: var(--border) !important; th { border-color: var(--border) !important; - color: var(--text-header, #0F1316); - font-size: var(--Text-size-sm, 14px); + color: var(--text-body); + font-family: var(--brand-font-family, var(--font-family)); + font-size: 12px; font-style: normal; - font-weight: 600; - line-height: var(--Text-line-height-sm, 22px); + font-weight: 500; + letter-spacing: 0.2px; + line-height: 18px; } } } @@ -1023,11 +1061,12 @@ formatting-text-editor { td { border-color: var(--border) !important; - color: var(--text-body); - font-size: var(--Text-size-sm, 14px); + color: var(--text-header); + font-family: var(--font-family); + font-size: 14px; font-style: normal; - font-weight: 500; - line-height: var(--Text-line-height-sm, 22px); + font-weight: 400; + line-height: 20px; } //&:last-child { @@ -1094,80 +1133,66 @@ div.mat-mdc-select-panel { } .mat-mdc-menu-panel { - display: inline-flex; - flex-direction: column; - align-items: flex-start; - border-radius: var(--rounded-8, 8px); - border: 1px solid var(--border, #EBEFF2) !important; - background: var(--bg, #FFF) !important; - box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.10); + border-radius: 12px !important; + border: none !important; + background: var(--card, #FFF) !important; + box-shadow: 0 2px 6px 2px rgba(0, 0, 0, 0.08), 0 1px 2px 0 rgba(0, 0, 0, 0.10) !important; + min-width: 220px !important; .mat-mdc-menu-content { - display: inline-flex; - padding: var(--28-px, 8px); - flex-direction: column; - align-items: flex-start; - gap: var(--14-px, 4px); - + padding: 8px 0 !important; - button { + button, + .mat-mdc-menu-item { display: flex; - height: 40px; - padding: var(--28-px, 8px) var(--416-px, 16px); + height: 36px; + min-height: 36px !important; + padding: 0 16px !important; + margin: 0 !important; + width: 100% !important; + box-sizing: border-box !important; align-items: center; - gap: var(--28-px, 8px); + gap: 12px; align-self: stretch; + border-radius: 0 !important; .mat-icon { margin: 0 !important; + width: 20px; + height: 20px; + font-size: 20px; + line-height: 20px; + color: var(--icon-secondary) !important; } - span { - color: var(--text-header, #0F1316) !important; - font-family: var(--font-family, "Nunito Sans"); - font-size: var(--Text-size-sm, 14px); - font-style: normal; - font-weight: 600; - line-height: var(--Text-line-height-sm, 22px); + span, + .mat-mdc-menu-item-text { + color: var(--text-header) !important; + font-family: var(--brand-font-family, var(--font-family)) !important; + font-size: 14px !important; + font-weight: 400 !important; + line-height: 20px !important; + letter-spacing: 0.1px; + } + + &:hover:not([aria-disabled="true"]) { + background: var(--table-row-hover) !important; } &[aria-disabled="true"] { cursor: not-allowed !important; + opacity: 0.5; } } .btn-delete-menu { - color: var(---error) !important; - span, mat-icon, .mat-mdc-menu-item-text { color: var(--error) !important; } + .mat-icon { color: var(--error) !important; } &:hover { - background: var(--error) !important; - - span, mat-icon, .mat-mdc-menu-item-text { - color: var(--btn-delete-text) !important; - } - } - } - - .mat-mdc-menu-item { - display: flex; - height: 40px; - padding: var(--28-px, 8px) var(--416-px, 16px); - align-items: flex-start; - gap: var(--28-px, 8px); - align-self: stretch; - min-height: unset !important; - - .mat-mdc-menu-item-text { - color: var(--text-header, #0F1316) !important; - font-family: var(--font-family, "Nunito Sans"); - font-size: var(--Text-size-sm, 14px) !important; - font-style: normal !important; - font-weight: 600 !important; - line-height: var(--Text-line-height-sm, 22px) !important; + background: var(--table-row-hover) !important; } } } @@ -1265,7 +1290,16 @@ div.mat-mdc-select-panel { } .mtx-grid .mat-table-sticky-left { - border-right: 1px solid var(--border, #EBEFF2) !important; + border-right: none !important; + background: var(--card) !important; +} +.mtx-grid .mat-mdc-row:hover .mat-table-sticky-left, +.mat-mdc-row:hover .mat-table-sticky-left { + background: var(--table-row-hover) !important; +} +.mtx-grid .mat-mdc-row.selected .mat-table-sticky-left, +.mtx-grid .mat-mdc-row[aria-selected="true"] .mat-table-sticky-left { + background: var(--selected-surface) !important; } .mat-mdc-slide-toggle { @@ -1406,14 +1440,25 @@ div.mat-mdc-select-panel { } th.mat-table-sticky-right { - border-left: 1px solid var(--border, #EBEFF2) !important; - box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05) !important; + border-left: none !important; + box-shadow: none !important; + background: var(--card) !important; } td.mat-table-sticky-right { - border-left: 1px solid var(--border, #EBEFF2) !important; - background: var(--bg, #FFF) !important; - box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05) !important; + border-left: none !important; + background: var(--card) !important; + box-shadow: none !important; +} + +.mtx-grid .mat-mdc-row:hover td.mat-table-sticky-right, +.mat-mdc-row:hover td.mat-table-sticky-right { + background: var(--table-row-hover) !important; +} + +.mtx-grid .mat-mdc-row.selected td.mat-table-sticky-right, +.mtx-grid .mat-mdc-row[aria-selected="true"] td.mat-table-sticky-right { + background: var(--selected-surface) !important; } diff --git a/eform-client/src/scss/themes/_eform.scss b/eform-client/src/scss/themes/_eform.scss new file mode 100644 index 0000000000..da0a032041 --- /dev/null +++ b/eform-client/src/scss/themes/_eform.scss @@ -0,0 +1,84 @@ +@use '@angular/material' as mat; + +$eform-font-family: "Nunito Sans", Roboto, "Helvetica Neue", sans-serif; + +$eform-tokens: ( + brand-family: $eform-font-family, + plain-family: $eform-font-family, + primary-palette: mat.$cyan-palette, + density-scale: 0, + button-shape: 20px, + icon-button-shape: 20px, + fab-shape: 20px, + dialog-shape: 20px, + card-shape: 12px, + chip-shape: 8px, + nav-item-height: 48px, + nav-item-radius: 9999px, + dropdown-option-height: 48px, + dropdown-panel-radius: 4px, + primary-button-height: 48px, + icon-button-size: 48px, + colors: ( + light: ( + primary: #289694, + primary-hover: #00716F, + primary-pressed: #005251, + primary-light: #F5FCFC, + primary-dark: #00716F, + on-primary: #FFFFFF, + link: #289694, + focus-ring: rgba(40, 150, 148, 0.24), + hover-surface: rgba(15, 19, 22, 0.06), + selected-surface: #289694, + table-header-bg: #FFFFFF, + table-row-hover: rgba(40, 150, 148, 0.06), + input-fill: transparent, + input-focus-underline: #289694, + bg: #FFFFFF, + text-header: #0F1316, + text-body: #7F868D, + card: #FFFFFF, + border: #e2e6e9, + nav-selected-surface: #289694, + nav-selected-text: #FFFFFF, + icon-secondary: #0F1316, + error: #F44336, + warning: #E2A01C, + btn-delete-text: #F3F5F7, + content-card-bg: #FFFFFF, + content-card-radius: 0, + content-card-margin: 0, + ), + dark: ( + primary: #4FCAC8, + primary-hover: #6FD9D7, + primary-pressed: #8CE4E2, + primary-light: #0D4746, + primary-dark: #00ADAB, + on-primary: #0F1316, + link: #4FCAC8, + focus-ring: rgba(79, 202, 200, 0.32), + hover-surface: rgba(243, 245, 247, 0.08), + selected-surface: #0D4746, + table-header-bg: #2D2F31, + table-row-hover: rgba(243, 245, 247, 0.06), + input-fill: #2D2F31, + input-focus-underline: #4FCAC8, + bg: #202122, + text-header: #F3F5F7, + text-body: #C1C5C9, + card: #2D2F31, + border: #37383A, + nav-selected-surface: #0D4746, + nav-selected-text: #4FCAC8, + icon-secondary: #FFFFFF, + error: #FF8282, + warning: #F4C25E, + btn-delete-text: #0F1316, + content-card-bg: #202122, + content-card-radius: 0, + content-card-margin: 0, + ), + ), +); diff --git a/eform-client/src/scss/themes/_theme-mixin.scss b/eform-client/src/scss/themes/_theme-mixin.scss new file mode 100644 index 0000000000..70d3ce34d8 --- /dev/null +++ b/eform-client/src/scss/themes/_theme-mixin.scss @@ -0,0 +1,161 @@ +@use 'sass:map'; +@use '@angular/material' as mat; + +@mixin apply-theme($tokens, $mode) { + $theme: mat.define-theme(( + color: ( + theme-type: $mode, + primary: map.get($tokens, primary-palette), + ), + typography: ( + brand-family: map.get($tokens, brand-family), + plain-family: map.get($tokens, plain-family), + ), + density: ( + scale: map.get($tokens, density-scale), + ), + )); + + @include mat.all-component-themes($theme); + + $colors: map.get($tokens, colors); + $mode-colors: map.get($colors, $mode); + + @include mat.button-overrides(( + filled-container-shape: map.get($tokens, button-shape), + outlined-container-shape: map.get($tokens, button-shape), + protected-container-shape: map.get($tokens, button-shape), + text-container-shape: map.get($tokens, button-shape), + filled-container-color: map.get($mode-colors, primary), + filled-label-text-color: map.get($mode-colors, on-primary), + filled-state-layer-color: map.get($mode-colors, on-primary), + filled-ripple-color: rgba(255, 255, 255, 0.24), + outlined-label-text-color: map.get($mode-colors, primary), + outlined-outline-color: map.get($mode-colors, border), + outlined-state-layer-color: map.get($mode-colors, primary), + text-label-text-color: map.get($mode-colors, primary), + text-state-layer-color: map.get($mode-colors, primary), + protected-container-color: map.get($mode-colors, card), + protected-label-text-color: map.get($mode-colors, primary), + protected-state-layer-color: map.get($mode-colors, primary), + tonal-container-color: map.get($mode-colors, primary-light), + tonal-label-text-color: map.get($mode-colors, primary-pressed), + tonal-state-layer-color: map.get($mode-colors, primary), + )); + + @include mat.form-field-overrides(( + filled-container-color: map.get($mode-colors, input-fill), + filled-focus-active-indicator-color: map.get($mode-colors, input-focus-underline), + filled-caret-color: map.get($mode-colors, primary), + filled-focus-label-text-color: map.get($mode-colors, primary), + outlined-focus-outline-color: map.get($mode-colors, primary), + outlined-focus-label-text-color: map.get($mode-colors, primary), + outlined-caret-color: map.get($mode-colors, primary), + )); + + @include mat.table-overrides(( + background-color: map.get($mode-colors, card), + header-headline-color: map.get($mode-colors, text-header), + row-item-label-text-color: map.get($mode-colors, text-header), + row-item-outline-color: map.get($mode-colors, border), + )); + + @include mat.list-overrides(( + list-item-hover-state-layer-color: map.get($mode-colors, primary), + list-item-focus-state-layer-color: map.get($mode-colors, primary), + list-item-selected-container-color: map.get($mode-colors, selected-surface), + list-item-label-text-color: map.get($mode-colors, text-header), + )); + + @include mat.icon-button-overrides(( + container-shape: map.get($tokens, icon-button-shape), + )); + + @include mat.fab-overrides(( + container-shape: map.get($tokens, fab-shape), + small-container-shape: map.get($tokens, fab-shape), + )); + + @include mat.dialog-overrides(( + container-shape: map.get($tokens, dialog-shape), + container-elevation-shadow: none, + actions-padding: 20px, + )); + + @include mat.card-overrides(( + elevated-container-shape: map.get($tokens, card-shape), + outlined-container-shape: map.get($tokens, card-shape), + filled-container-shape: map.get($tokens, card-shape), + )); + + @include mat.chips-overrides(( + container-shape-radius: map.get($tokens, chip-shape), + )); + + --font-family: #{map.get($tokens, plain-family)}; + --brand-font-family: #{map.get($tokens, brand-family)}; + --nav-item-height: #{map.get($tokens, nav-item-height)}; + --nav-item-radius: #{map.get($tokens, nav-item-radius)}; + --dropdown-option-height: #{map.get($tokens, dropdown-option-height)}; + --dropdown-panel-radius: #{map.get($tokens, dropdown-panel-radius)}; + --primary-button-height: #{map.get($tokens, primary-button-height)}; + --icon-button-size: #{map.get($tokens, icon-button-size)}; + --primary: #{map.get($mode-colors, primary)}; + --primary-hover: #{map.get($mode-colors, primary-hover)}; + --primary-pressed: #{map.get($mode-colors, primary-pressed)}; + --primary-light: #{map.get($mode-colors, primary-light)}; + --primary-dark: #{map.get($mode-colors, primary-dark)}; + --on-primary: #{map.get($mode-colors, on-primary)}; + --link: #{map.get($mode-colors, link)}; + --focus-ring: #{map.get($mode-colors, focus-ring)}; + --hover-surface: #{map.get($mode-colors, hover-surface)}; + --selected-surface: #{map.get($mode-colors, selected-surface)}; + --nav-selected-surface: #{map.get($mode-colors, nav-selected-surface)}; + --nav-selected-text: #{map.get($mode-colors, nav-selected-text)}; + --table-header-bg: #{map.get($mode-colors, table-header-bg)}; + --table-row-hover: #{map.get($mode-colors, table-row-hover)}; + --input-fill: #{map.get($mode-colors, input-fill)}; + --input-focus-underline: #{map.get($mode-colors, input-focus-underline)}; + --bg: #{map.get($mode-colors, bg)}; + --text-header: #{map.get($mode-colors, text-header)}; + --text-body: #{map.get($mode-colors, text-body)}; + --card: #{map.get($mode-colors, card)}; + --border: #{map.get($mode-colors, border)}; + --icon-secondary: #{map.get($mode-colors, icon-secondary)}; + --error: #{map.get($mode-colors, error)}; + --warning: #{map.get($mode-colors, warning)}; + --btn-delete-text: #{map.get($mode-colors, btn-delete-text)}; + --content-card-bg: #{map.get($mode-colors, content-card-bg)}; + --content-card-radius: #{map.get($mode-colors, content-card-radius)}; + --content-card-margin: #{map.get($mode-colors, content-card-margin)}; + + --mdc-theme-primary: #{map.get($mode-colors, primary)}; + --mdc-theme-on-primary: #{map.get($mode-colors, on-primary)}; + --mdc-theme-primary-dark: #{map.get($mode-colors, primary-dark)}; + --mdc-theme-on-primary-dark: #{map.get($mode-colors, on-primary)}; + --mat-app-background-color: #{map.get($mode-colors, bg)}; + --mat-table-row-item-outline-color: #{map.get($mode-colors, border)}; + --mat-table-background-color: #{map.get($mode-colors, card)}; + --mat-table-row-item-container-color: #{map.get($mode-colors, card)}; + --mat-table-header-container-color: #{map.get($mode-colors, table-header-bg)}; + --mat-card-elevated-container-color: #{map.get($mode-colors, card)}; + --mat-card-elevated-container-elevation: 0; + --mat-card-filled-container-color: #{map.get($mode-colors, card)}; + --mat-sidenav-content-background-color: #{map.get($mode-colors, bg)}; + --mdc-filled-button-container-color: #{map.get($mode-colors, primary)}; + --mdc-filled-button-label-text-color: #{map.get($mode-colors, on-primary)}; + --mdc-protected-button-container-color: #{map.get($mode-colors, card)}; + --mdc-protected-button-label-text-color: #{map.get($mode-colors, primary)}; + --mdc-outlined-button-label-text-color: #{map.get($mode-colors, primary)}; + --mdc-outlined-button-outline-color: #{map.get($mode-colors, border)}; + --mdc-text-button-label-text-color: #{map.get($mode-colors, primary)}; + + ::selection { + background: #{map.get($mode-colors, primary)}; + color: #{map.get($mode-colors, on-primary)}; + } + ::-moz-selection { + background: #{map.get($mode-colors, primary)}; + color: #{map.get($mode-colors, on-primary)}; + } +} diff --git a/eform-client/src/scss/themes/_workspace.scss b/eform-client/src/scss/themes/_workspace.scss new file mode 100644 index 0000000000..cb5ee3bab2 --- /dev/null +++ b/eform-client/src/scss/themes/_workspace.scss @@ -0,0 +1,77 @@ +@use '@angular/material' as mat; + +$workspace-brand-family: ('Google Sans', 'Google Sans Text', Roboto, sans-serif); +$workspace-plain-family: (Roboto, Arial, sans-serif); + +$workspace-tokens: ( + brand-family: $workspace-brand-family, + plain-family: $workspace-plain-family, + primary-palette: mat.$azure-palette, + density-scale: -1, + button-shape: 20px, + icon-button-shape: 20px, + fab-shape: 16px, + dialog-shape: 28px, + card-shape: 16px, + chip-shape: 8px, + colors: ( + light: ( + primary: #0b57d0, + primary-hover: #0842a0, + primary-pressed: #062e6f, + primary-light: #d3e3fd, + primary-dark: #001d35, + on-primary: #ffffff, + link: #1a73e8, + focus-ring: #0b57d0, + hover-surface: rgba(11, 87, 208, 0.08), + selected-surface: #d3e3fd, + nav-selected-surface: #e8eaed, + table-header-bg: #ffffff, + table-row-hover: #f1f3f4, + input-fill: #f0f4f9, + input-focus-underline: #0b57d0, + bg: #f0f4f9, + text-header: #1f1f1f, + text-body: #5f6368, + card: #ffffff, + border: #dde3ea, + icon-secondary: #444746, + error: #b3261e, + warning: #b58d0e, + btn-delete-text: #ffffff, + content-card-bg: #FFFFFF, + content-card-radius: 16px, + content-card-margin: 16px, + ), + dark: ( + primary: #a8c7fa, + primary-hover: #c2d7fc, + primary-pressed: #d3e3fd, + primary-light: #062e6f, + primary-dark: #0842a0, + on-primary: #062e6f, + link: #a8c7fa, + focus-ring: #a8c7fa, + hover-surface: rgba(168, 199, 250, 0.10), + selected-surface: #062e6f, + nav-selected-surface: #3c4043, + table-header-bg: #1f2024, + table-row-hover: #2d2e33, + input-fill: #1f2024, + input-focus-underline: #a8c7fa, + bg: #1b1b1f, + text-header: #e3e3e3, + text-body: #c4c7c5, + card: #28292e, + border: #3c4043, + icon-secondary: #c4c7c5, + error: #f2b8b5, + warning: #f4c25e, + btn-delete-text: #062e6f, + content-card-bg: #28292e, + content-card-radius: 16px, + content-card-margin: 16px, + ), + ), +);