Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: ["main"]

permissions:
contents: read
contents: write
pull-requests: write

jobs:
Expand Down Expand Up @@ -48,6 +48,17 @@ jobs:
path: screenshot-results/*.png
retention-days: 30

- name: Commit updated README screenshots
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docs/assets/escalated_admin_1.png docs/assets/escalated_admin_2.png
git diff --cached --quiet && echo "No screenshot changes" || {
git commit -m "chore: update README screenshots from Storybook"
git push
}

- name: Comment on PR with screenshot summary
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
Expand Down
87 changes: 87 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
import { computed } from 'vue';
import './storybook.css';

const panelDarkDefaults = {
'--esc-panel-bg': '#000000',
'--esc-panel-sidebar-bg': '#0a0a0a',
'--esc-panel-topbar-bg': 'rgba(0,0,0,0.8)',
'--esc-panel-surface': 'rgba(23,23,23,0.6)',
'--esc-panel-surface-alt': '#0a0a0a',
'--esc-panel-border': 'rgba(255,255,255,0.06)',
'--esc-panel-border-input': 'rgba(255,255,255,0.1)',
'--esc-panel-text': '#ffffff',
'--esc-panel-text-secondary': '#e5e5e5',
'--esc-panel-text-tertiary': '#a3a3a3',
'--esc-panel-text-muted': '#737373',
'--esc-panel-accent': '#06b6d4',
'--esc-panel-accent-secondary': '#8b5cf6',
'--esc-panel-accent-hover': '#22d3ee',
'--esc-panel-accent-secondary-hover': '#a78bfa',
'--esc-panel-hover': 'rgba(255,255,255,0.03)',
'--esc-panel-active': 'rgba(255,255,255,0.08)',
};

const panelPresets = {
'default-dark': panelDarkDefaults,
'default-light': {
'--esc-panel-bg': '#f9fafb',
'--esc-panel-sidebar-bg': '#ffffff',
'--esc-panel-topbar-bg': 'rgba(255,255,255,0.95)',
'--esc-panel-surface': '#ffffff',
'--esc-panel-surface-alt': '#f9fafb',
'--esc-panel-border': '#e5e7eb',
'--esc-panel-border-input': '#d1d5db',
'--esc-panel-text': '#111827',
'--esc-panel-text-secondary': '#374151',
'--esc-panel-text-tertiary': '#6b7280',
'--esc-panel-text-muted': '#9ca3af',
'--esc-panel-accent': '#3b82f6',
'--esc-panel-accent-secondary': '#6366f1',
'--esc-panel-accent-hover': '#2563eb',
'--esc-panel-accent-secondary-hover': '#818cf8',
'--esc-panel-hover': 'rgba(0,0,0,0.02)',
'--esc-panel-active': '#eff6ff',
},
'custom-brand': {
'--esc-panel-bg': '#0f0f23',
'--esc-panel-sidebar-bg': '#1a1a2e',
'--esc-panel-topbar-bg': 'rgba(15,15,35,0.8)',
'--esc-panel-surface': '#16213e',
'--esc-panel-surface-alt': '#0f0f23',
'--esc-panel-border': 'rgba(255,255,255,0.08)',
'--esc-panel-border-input': 'rgba(255,255,255,0.12)',
'--esc-panel-text': '#ffffff',
'--esc-panel-text-secondary': '#a0aec0',
'--esc-panel-text-tertiary': '#718096',
'--esc-panel-text-muted': '#4a5568',
'--esc-panel-accent': '#e94560',
'--esc-panel-accent-secondary': '#ff6b6b',
'--esc-panel-accent-hover': '#c81e45',
'--esc-panel-accent-secondary-hover': '#ff8787',
'--esc-panel-hover': 'rgba(255,255,255,0.03)',
'--esc-panel-active': 'rgba(233,69,96,0.12)',
},
};

function applyPanelPreset(el, presetName) {
const preset = panelPresets[presetName] || panelDarkDefaults;
for (const [prop, value] of Object.entries(preset)) {
el.style.setProperty(prop, value);
}
}

/** @type { import('@storybook/vue3-vite').Preview } */
const preview = {
parameters: {
Expand Down Expand Up @@ -29,10 +98,28 @@ const preview = {
dynamicTitle: true,
},
},
panelTheme: {
name: 'Panel Theme',
description: 'Panel theme preset',
defaultValue: 'default-dark',
toolbar: {
icon: 'grid',
items: [
{ value: 'default-dark', title: 'Default Dark' },
{ value: 'default-light', title: 'Default Light' },
{ value: 'custom-brand', title: 'Custom Brand' },
],
dynamicTitle: true,
},
},
},
decorators: [
(story, context) => {
const theme = context.globals.theme || 'dark';
const panelTheme = context.globals.panelTheme || 'default-dark';

// Apply panel theme CSS variables to document root
applyPanelPreset(document.documentElement, panelTheme);

if (theme === 'side-by-side') {
return {
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,14 @@ All the Vue 3 + Inertia.js components that power the Escalated UI. These are ide

## 📸 Screenshots

> Screenshots are auto-generated from Storybook via the [component-screenshots](.github/workflows/screenshots.yml) workflow.

<p align="center">
<img src="docs/assets/escalated_admin_1.png" alt="Escalated Admin Screenshot 1" width="800" />
<img src="docs/assets/escalated_admin_1.png" alt="Escalated dashboard — StatsCard grid with metrics and trends" width="800" />
</p>

<p align="center">
<img src="docs/assets/escalated_admin_2.png" alt="Escalated Admin Screenshot 2" width="800" />
<img src="docs/assets/escalated_admin_2.png" alt="Escalated analytics — KPI cards with icons and trend indicators" width="800" />
</p>

### Pages
Expand Down
Binary file modified docs/assets/escalated_admin_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/escalated_admin_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
132 changes: 132 additions & 0 deletions docs/plans/2026-03-01-panel-theming-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Panel Theming Design

**Date:** 2026-03-01
**Branch:** `feature/panel-theming`
**Status:** Approved

## Problem

The Escalated framework allows theming for the customer-facing frontend via `EscalatedPlugin` (primary color, border radius, font family), but the admin and agent panels are locked to a hardcoded dark theme. Users who need to whitelabel or brand these panels have no mechanism to do so.

## Goals

- Enable theming/branding for both admin and agent panels (they share a visual identity)
- Support two tiers: quick customization (accent color, logo, mode) and full whitelabel (granular token overrides)
- Support both dark and light mode for panels
- Allow custom logo via URL or Vue component
- Maintain zero-config backwards compatibility — default tokens reproduce the current look exactly

## Approach

CSS custom properties (`--esc-panel-*`) applied to `:root`, consistent with the existing frontend theming pattern. All admin/agent hardcoded Tailwind color classes are migrated to `var()` references.

## Token System

~15 base tokens with dark and light default palettes:

| Token | Purpose | Dark Default | Light Default |
|-------|---------|-------------|--------------|
| `--esc-panel-bg` | Page background | `#000000` | `#f9fafb` |
| `--esc-panel-sidebar-bg` | Sidebar background | `#0a0a0a` | `#ffffff` |
| `--esc-panel-topbar-bg` | Top bar background | `rgba(0,0,0,0.8)` | `rgba(255,255,255,0.95)` |
| `--esc-panel-surface` | Cards, panels | `rgba(23,23,23,0.6)` | `#ffffff` |
| `--esc-panel-surface-alt` | Nested surfaces, inputs | `#0a0a0a` | `#f9fafb` |
| `--esc-panel-border` | Primary borders | `rgba(255,255,255,0.06)` | `#e5e7eb` |
| `--esc-panel-border-input` | Input borders | `rgba(255,255,255,0.1)` | `#d1d5db` |
| `--esc-panel-text` | Primary text | `#ffffff` | `#111827` |
| `--esc-panel-text-secondary` | Labels, body text | `#e5e5e5` | `#374151` |
| `--esc-panel-text-muted` | Hints, placeholders | `#737373` | `#6b7280` |
| `--esc-panel-accent` | Primary accent | `#06b6d4` | `#3b82f6` |
| `--esc-panel-accent-secondary` | Secondary accent (gradient end) | `#8b5cf6` | `#6366f1` |
| `--esc-panel-accent-hover` | Accent hover state | `#22d3ee` | `#2563eb` |
| `--esc-panel-hover` | Row/item hover bg | `rgba(255,255,255,0.03)` | `#f9fafb` |
| `--esc-panel-active` | Active nav item bg | `rgba(255,255,255,0.08)` | `#eff6ff` |

**Not tokenized:** Status colors (open/closed/escalated/etc.) and priority colors remain hardcoded — they are semantic indicators, not brand colors.

## Plugin API

### Tier 1 — Quick Customization

```js
app.use(EscalatedPlugin, {
theme: {
primary: '#3b82f6', // existing frontend theming (unchanged)
panel: {
mode: 'dark', // 'dark' | 'light'
accent: '#e94560', // overrides accent + accent-secondary
appName: 'HelpDesk Pro',
logo: '/img/brand-logo.svg', // URL string
}
}
})
```

### Tier 2 — Full Whitelabel

```js
app.use(EscalatedPlugin, {
theme: {
panel: {
mode: 'dark',
appName: 'ClientDesk',
logo: MyLogoComponent, // Vue component
accent: '#e94560',
accentSecondary: '#ff6b6b',
bg: '#0f0f23',
sidebarBg: '#1a1a2e',
topbarBg: '#0f0f23',
surface: '#16213e',
surfaceAlt: '#0f0f23',
border: 'rgba(255,255,255,0.08)',
borderInput: 'rgba(255,255,255,0.12)',
text: '#ffffff',
textSecondary: '#a0aec0',
textMuted: '#718096',
}
}
})
```

### Logo Detection

```js
// String → image URL → renders <img :src="logo">
// Object/Function → Vue component → renders <component :is="logo">
```

## Migration Scope

### Changes:

1. **`plugin.js`** — New `applyPanelTheme()` function with dark/light default palettes. `provide()` for logo, appName, panel config.

2. **`EscalatedLayout.vue`** — Admin sidebar, topbar, and agent nav switch from hardcoded Tailwind colors to `var(--esc-panel-*)` references. Logo section becomes dynamic (default SVG / URL / component). App name becomes dynamic.

3. **All admin pages (~25)** — Card containers, form inputs, buttons, tables, text all migrate to `var()`. Gradient buttons become `from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)]`.

4. **Shared components in dark-mode branch** — Components using `inject('esc-dark')` have their dark-mode styling branch updated to use `var()` references.

5. **Storybook** — Add panel theme selector to preview different theme configurations.

### No changes:

- Customer-facing pages (light mode) — untouched
- Status/priority semantic colors — hardcoded
- Existing `theme.primary`, `theme.radius`, `theme.fontFamily` — untouched
- Component APIs/props — no breaking changes

## Color Scheme Handling

When `mode: 'light'` is set:
- The `color-scheme: dark` inline style on admin/agent containers switches to `color-scheme: light`
- The `esc-dark` injection value respects the mode setting (light mode panels inject `false`)
- Components that use `inject('esc-dark')` will automatically pick their light-mode branch
- The light default palette is applied via CSS custom properties

## Backwards Compatibility

- Zero-config behavior is identical to current — dark tokens default to exact current values
- Existing `theme` options continue to work unchanged
- No new peer dependencies
- No build step changes required for consumers
Loading