semi functional

This commit is contained in:
Matt Batchelder
2026-02-04 10:00:40 -05:00
parent 287e03da42
commit 2153d3c725
28 changed files with 8451 additions and 4361 deletions

View File

@@ -1,361 +0,0 @@
# Theme Customization Cookbook
Quick recipes for common customization tasks in your Xibo CMS theme.
## 1. Change Primary Brand Color
**File:** `css/override.css`
Find the `:root` selector and update:
```css
:root {
--color-primary: #006bb3; /* Change from blue #2563eb to custom blue */
--color-primary-dark: #004c80; /* Darker shade for hover/active states */
--color-primary-light: #1a9ad1; /* Lighter shade for backgrounds */
--color-primary-lighter: #d4e6f1; /* Very light for highlights */
}
```
This single change affects:
- Header bar background
- Sidebar primary colors
- Buttons, links, and focus states
- Widget title bars
## 2. Use a Custom Font
**File:** `css/override.css`
Replace the `--font-family-base` variable:
```css
:root {
/* Option A: Google Font (add to <head> in Twig override) */
--font-family-base: "Inter", "Segoe UI", sans-serif;
/* Option B: Local font file */
@font-face {
font-family: "MyCustomFont";
src: url('../fonts/my-font.woff2') format('woff2');
}
--font-family-base: "MyCustomFont", sans-serif;
}
```
To use Google Fonts, add this line to a Twig template (e.g., in a view override):
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
```
## 3. Implement a Company Logo in Header
**Files:** `css/override.css` (styling), `img/` (asset)
1. Replace logo files in `custom/otssignange/img/`:
- `xibologo.png` (header logo, ~40x40px)
- `192x192.png` (app icon for app manifest)
- `512x512.png` (splash/bookmarklet icon)
2. If you need to style the logo more prominently, add to `override.css`:
```css
.navbar-brand {
display: flex;
align-items: center;
gap: var(--space-2);
padding-left: var(--space-4);
}
.navbar-brand img {
height: 40px;
width: auto;
}
```
## 4. Darken the Sidebar
**File:** `css/override.css`
Update sidebar color tokens:
```css
:root {
--color-surface: #2d3748; /* Darker gray instead of light */
--color-text-primary: #ffffff; /* White text on dark background */
}
#sidebar-wrapper {
background-color: #1a202c; /* Even darker for contrast */
}
ul.sidebar .sidebar-list a {
color: #cbd5e1; /* Light gray text */
}
ul.sidebar .sidebar-list a:hover {
background-color: #2d3748; /* Slightly lighter on hover */
color: #ffffff;
}
```
## 5. Increase Widget Spacing & Padding
**File:** `css/override.css`
Modify spacing tokens to make everything roomier:
```css
:root {
/* Scale up all spacing */
--space-4: 1.25rem; /* was 1rem (16px → 20px) */
--space-6: 2rem; /* was 1.5rem (24px → 32px) */
--space-8: 2.75rem; /* was 2rem (32px → 44px) */
}
.widget {
padding: var(--space-8); /* Uses new token value */
}
```
## 6. Remove Shadows (Flat Design)
**File:** `css/override.css`
Set all shadows to none:
```css
:root {
--shadow-xs: none;
--shadow-sm: none;
--shadow-base: none;
--shadow-md: none;
--shadow-lg: none;
--shadow-xl: none;
}
```
## 7. Customize Button Styles
**File:** `css/override.css`
Make buttons larger with more rounded corners:
```css
button,
.btn {
padding: var(--space-4) var(--space-6); /* Increase from var(--space-3) var(--space-4) */
border-radius: var(--radius-xl); /* Make more rounded */
font-weight: var(--font-weight-semibold); /* Make text bolder */
text-transform: uppercase; /* OPTIONAL: Uppercase labels */
letter-spacing: 0.05em; /* OPTIONAL: Wider letter spacing */
}
```
## 8. Force Light Mode (Disable Dark Mode)
**File:** `css/override.css`
Remove the dark mode media query or override it:
```css
/* Delete or comment out this section: */
/*
@media (prefers-color-scheme: dark) {
:root { ... }
}
*/
/* OR force light mode explicitly: */
:root {
color-scheme: light; /* Tells browser to not apply dark UI elements */
}
/* Force light colors even if system prefers dark */
@media (prefers-color-scheme: dark) {
:root {
/* Keep all light mode values, don't override */
}
}
```
## 9. Add a Custom Alert Style
**File:** `css/override.css`
Append a new alert variant (e.g., for secondary notifications):
```css
.alert-secondary {
background-color: #e2e8f0;
border-color: #cbd5e1;
color: #334155;
}
.alert-secondary a {
color: #2563eb;
font-weight: var(--font-weight-semibold);
}
```
Use in Xibo: Apply `.alert alert-secondary` class to a notification element.
## 10. Improve Form Focus States
**File:** `css/override.css`
Make focused form inputs more prominent:
```css
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 4px var(--color-primary-lighter), /* Outer glow */
0 0 0 1px var(--color-primary); /* Inner border */
background-color: #fafbff; /* Subtle highlight */
}
```
## 11. Create a Compact Theme Variant
**File:** `css/override.css`
Add a utility class for a denser layout:
```css
.theme-compact {
--space-4: 0.75rem;
--space-6: 1rem;
--font-size-base: 0.875rem; /* Slightly smaller text */
}
/* Apply to body or any container */
body.theme-compact {
/* All tokens inherit new values */
}
```
Then toggle with a Twig override or JS:
```javascript
document.body.classList.toggle('theme-compact');
```
## 12. Modify Widget Title Bar Colors
**File:** `css/override.css`
Make widget titles more distinctive:
```css
.widget .widget-title {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: var(--color-text-inverse);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: var(--font-size-sm);
padding: var(--space-6);
border-bottom: 2px solid var(--color-primary-dark);
}
```
## 13. Style Table Headers Distinctly
**File:** `css/override.css`
Make tables look more modern:
```css
thead {
background: linear-gradient(to bottom, var(--color-primary-lighter), var(--color-gray-100));
}
th {
color: var(--color-primary);
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: var(--font-size-sm);
padding: var(--space-6) var(--space-4);
border-bottom: 2px solid var(--color-primary);
}
tbody tr:nth-child(odd) {
background-color: var(--color-gray-50);
}
tbody tr:hover {
background-color: var(--color-primary-lighter);
}
```
## 14. Enable Full Dark Mode in Browser
Some users may have `prefers-color-scheme: dark` set. To test locally:
**Chrome DevTools:**
1. Open DevTools (F12)
2. Ctrl+Shift+P (or Cmd+Shift+P on Mac)
3. Type "rendering"
4. Select "Show Rendering"
5. Scroll to "Prefers color scheme" and select "dark"
6. Refresh page
**Firefox:**
1. about:config
2. Search for `ui.systemUsesDarkTheme`
3. Set to `1` for dark mode
## 15. Add Custom Utility Classes
**File:** `css/override.css`
Extend the theme with custom utilities at the end:
```css
/* Custom utilities */
.text-primary {
color: var(--color-primary);
}
.bg-primary {
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.border-primary {
border-color: var(--color-primary);
}
.opacity-50 {
opacity: 0.5;
}
.cursor-pointer {
cursor: pointer;
}
.no-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
```
---
**Pro Tips:**
- Always test changes in a staging Xibo instance before deploying to production
- Use browser DevTools to inspect elements and live-edit CSS before making permanent changes
- Keep a backup of your original CSS before making large modifications
- Document any custom changes you make in comments within the CSS file for future reference

View File

@@ -1,175 +0,0 @@
# Deployment Checklist & Quick Start
## Pre-Deployment Checklist
- [ ] Review the modern theme changes in [README.md](README.md)
- [ ] Test CSS syntax validation (no errors reported)
- [ ] Backup existing `custom/otssignange/css/override.css`
- [ ] Verify asset paths (logos, preview images)
- [ ] Check browser compatibility requirements
- [ ] Test on your development Xibo instance first
- [ ] Verify dark mode toggle if using system preference
- [ ] Test responsive layout on mobile devices
## Installation Steps
### Option A: Direct File Copy
```bash
# Navigate to your Xibo installation root
cd /path/to/xibo
# Backup original theme files
cp web/theme/custom/otssignange/css/override.css web/theme/custom/otssignange/css/override.css.backup
cp web/theme/custom/otssignange/css/html-preview.css web/theme/custom/otssignange/css/html-preview.css.backup
# Copy new theme files
cp /Users/matt/dev/theme/custom/otssignange/css/override.css web/theme/custom/otssignange/css/
cp /Users/matt/dev/theme/custom/otssignange/css/html-preview.css web/theme/custom/otssignange/css/
cp /Users/matt/dev/theme/custom/otssignange/css/client.css web/theme/custom/otssignange/css/
# Verify files copied
ls -la web/theme/custom/otssignange/css/
```
### Option B: Using Git (if version-controlled)
```bash
cd /path/to/xibo
git checkout web/theme/custom/otssignange/css/
# Or manually merge your changes with the new files
```
## Post-Deployment Validation
1. **Clear Xibo Cache** (if applicable):
```bash
# Xibo may cache CSS—clear if using PHP APC or similar
rm -rf web/uploads/temp/*
```
2. **Verify in Browser**:
- Open Xibo CMS admin interface
- Inspect elements for CSS color changes
- Check Network tab for CSS file loads (should see override.css)
- Verify no CSS errors in browser console
3. **Test Key Features**:
- [ ] Login page displays correctly
- [ ] Header bar shows primary color
- [ ] Sidebar navigation is styled properly
- [ ] Dashboard widgets render as cards with shadows
- [ ] Links have correct color and hover state
- [ ] Forms have proper focus states (blue outline)
- [ ] Mobile layout: open DevTools (F12) and resize to <640px
- [ ] Sidebar collapses into hamburger menu on mobile
- [ ] Dark mode: open DevTools → Rendering → Prefers color scheme: dark
4. **Check Asset Loading**:
- Verify `xibologo.png` displays in header
- Check preview splash screen background loads
- Confirm favicon appears in browser tab
## Rollback Plan
If issues occur:
```bash
cd /path/to/xibo
# Restore backup
cp web/theme/custom/otssignange/css/override.css.backup web/theme/custom/otssignange/css/override.css
cp web/theme/custom/otssignange/css/html-preview.css.backup web/theme/custom/otssignange/css/html-preview.css
# Optional: Remove new client.css if causing issues
rm web/theme/custom/otssignange/css/client.css
# Clear cache
rm -rf web/uploads/temp/*
# Refresh browser (Ctrl+Shift+R for hard refresh)
```
## Browser Support
### Fully Supported (CSS Variables)
- Chrome/Edge 49+
- Firefox 31+
- Safari 9.1+
- Opera 36+
- Mobile browsers (iOS Safari 9.3+, Chrome Mobile 49+)
### Partial Support (Fallbacks Recommended)
- IE 11 and below: Not supported
To add IE11 fallbacks, modify `override.css` (advanced):
```css
/* Fallback for older browsers */
.widget {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); /* IE fallback */
box-shadow: var(--shadow-sm);
}
```
## Performance Notes
- **CSS File Size**: `override.css` is ~35KB (gzipped ~8KB)
- **Variables**: 70+ CSS variables—negligible performance impact
- **Dark Mode**: Uses `prefers-color-scheme` media query (no JavaScript required)
- **Responsive**: Mobile-first approach—efficient layout recalculation
## Next Steps
1. **Optional Enhancements**:
- Add SVG icon sprite to `img/` for consistent iconography
- Create Twig view overrides for deeper layout customization
- Implement user-controlled dark mode toggle
2. **Documentation**:
- See [CUSTOMIZATION.md](CUSTOMIZATION.md) for 15+ customization recipes
- See [README.md](README.md) for full feature documentation
3. **Testing in CI/CD**:
- Add CSS linter (stylelint) to your build pipeline
- Validate HTML/CSS in staging before production push
## Support & Troubleshooting
### Issue: CSS Not Loading
**Solution**:
- Hard refresh browser: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
- Check browser console for 404 errors on CSS files
- Verify file permissions: `chmod 644 override.css`
### Issue: Colors Look Wrong
**Solution**:
- Check if system dark mode is enabled (see Post-Deployment Validation)
- Verify `--color-primary` value in `:root` matches intended brand color
- Test in different browsers
### Issue: Sidebar Doesn't Collapse on Mobile
**Solution**:
- Verify viewport meta tag is in Xibo's `<head>`: `<meta name="viewport" content="width=device-width, initial-scale=1">`
- Check browser DevTools for responsive mode enabled
- Ensure no custom CSS is overriding the media query
### Issue: Fonts Not Loading
**Solution**:
- Verify system fonts are available (`-apple-system`, `Segoe UI`, etc.)
- If using Google Fonts, check internet connectivity
- Add font-family fallback: `-fallback-font, sans-serif`
---
## Quick Links
- [Theme README](README.md) — Feature overview and tokens reference
- [Customization Cookbook](CUSTOMIZATION.md) — 15+ customization recipes
- [Xibo Developer Docs](https://account.xibosignage.com/docs/developer/)
- [Config Reference](custom/otssignange/config.php)
---
**Version**: 1.0.0 (Modern)
**Last Updated**: February 2026
**Status**: Ready for Production

View File

@@ -1,391 +0,0 @@
# Design System Reference Card
A quick visual and technical reference for the OTS Signage Modern Theme.
## 🎨 Color Palette
### Brand Colors
```
--color-primary #2563eb ████████████ Blue
--color-primary-dark #1d4ed8 ██████████ Darker Blue
--color-primary-light #3b82f6 ██████████████ Lighter Blue
--color-secondary #7c3aed ████████████ Purple
```
### Status Colors
```
--color-success #10b981 ██████████ Green
--color-warning #f59e0b ██████████ Amber
--color-danger #ef4444 ██████████ Red
--color-info #0ea5e9 ██████████ Cyan
```
### Gray Scale (Neutral)
```
Level Color Hex Usage
50 Very Light #f9fafb Backgrounds, light surfaces
100 Light #f3f4f6 Hover states, borders
200 Light Gray #e5e7eb Borders, dividers
300 Gray #d1d5db Form inputs, disabled
400 Gray #9ca3af Placeholder text
500 Medium Gray #6b7280 Secondary text
600 Dark Gray #4b5563 Body text, labels
700 Darker Gray #374151 Headings
800 Very Dark #1f2937 Primary text
900 Darkest #111827 High contrast text
```
### Semantic Colors
```
--color-background #ffffff (dark: #0f172a)
--color-surface #f9fafb (dark: #1e293b)
--color-surface-elevated #ffffff (dark: #334155)
--color-text-primary #1f2937 (dark: #f1f5f9)
--color-text-secondary #6b7280 (dark: #cbd5e1)
--color-text-tertiary #9ca3af (dark: #94a3b8)
--color-border #e5e7eb (dark: #475569)
```
---
## 📝 Typography Scale
### Font Family
```
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif
--font-family-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Menlo, Courier, monospace
```
### Font Sizes
```
Size Pixels Usage
--font-size-xs 12px Small labels, badges, captions
--font-size-sm 14px Form hints, table cells, small text
--font-size-base 16px Body text, default size
--font-size-lg 18px Subheadings, callouts
--font-size-xl 20px Section headings
--font-size-2xl 24px Page headings
--font-size-3xl 30px Large headings
--font-size-4xl 36px Main titles
```
### Font Weights
```
Weight Value Usage
--font-weight-normal 400 Body text, regular content
--font-weight-medium 500 Form labels, emphasis
--font-weight-semibold 600 Headings, strong emphasis
--font-weight-bold 700 Major headings, highlights
```
### Line Heights
```
Height Value Usage
--line-height-tight 1.25 Tight headings
--line-height-snug 1.375 Subheadings
--line-height-normal 1.5 Default, body text
--line-height-relaxed 1.625 Large text blocks
--line-height-loose 2 Extra spacing
```
---
## 📏 Spacing Scale (8px base unit)
```
Token Pixels CSS Rem
--space-1 4px 0.25rem Tightest spacing
--space-2 8px 0.5rem Small padding
--space-3 12px 0.75rem Medium-small
--space-4 16px 1rem Standard padding
--space-5 20px 1.25rem Medium spacing
--space-6 24px 1.5rem Default margins
--space-7 28px 1.75rem Large spacing
--space-8 32px 2rem Section spacing
--space-10 40px 2.5rem Large spacing
--space-12 48px 3rem Very large
--space-16 64px 4rem Extra large
--space-20 80px 5rem Massive spacing
```
**Usage Example:**
```css
.widget {
padding: var(--space-6); /* 24px all sides */
margin-bottom: var(--space-8); /* 32px below */
gap: var(--space-4); /* 16px between items */
}
```
---
## 🎭 Shadows (Elevation System)
```
Level Shadow Usage
none none No elevation
xs 0 1px 2px 0 rgba(0, 0, 0, 0.05) Subtle depth
sm 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 ... Small cards
base 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px ... Default cards
md 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px ... Medium elevation
lg 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px ... Large hover
xl 0 25px 50px -12px rgba(0, 0, 0, 0.25) Maximum depth
```
---
## 🔲 Border Radius
```
Token Pixels Usage
--radius-none 0px Sharp corners
--radius-sm 4px Minimal rounding
--radius-base 6px Default input fields
--radius-md 8px Standard components
--radius-lg 12px Cards, modals
--radius-xl 16px Large containers
--radius-2xl 24px Extra rounded
--radius-full 9999px Fully rounded (pills, circles)
```
---
## ⚡ Transitions
```
Token Duration Easing Usage
--transition-fast 150ms ease-in-out Hover, quick interactions
--transition-base 200ms ease-in-out Default, UI changes
--transition-slow 300ms ease-in-out Page transitions, major changes
```
**Usage:**
```css
a {
transition: color var(--transition-fast);
}
.widget {
transition: box-shadow var(--transition-base), transform var(--transition-base);
}
```
---
## 📱 Responsive Breakpoints
```
Name Width Use Case
sm 640px Mobile phones (landscape)
md 768px Tablets
lg 1024px Desktop screens
xl 1280px Large desktops
2xl 1536px Ultra-wide displays
```
**Usage Pattern (Mobile-First):**
```css
/* Mobile first (default) */
.widget {
grid-template-columns: 1fr;
}
/* Tablets and up */
@media (min-width: 768px) {
.widget {
grid-template-columns: repeat(2, 1fr);
}
}
/* Desktops and up */
@media (min-width: 1024px) {
.widget {
grid-template-columns: repeat(3, 1fr);
}
}
```
---
## 🎯 Component Reference
### Buttons
```css
.btn {
padding: var(--space-3) var(--space-4); /* 12px × 16px */
border-radius: var(--radius-md); /* 8px */
font-weight: var(--font-weight-medium); /* 500 */
font-size: var(--font-size-base); /* 16px */
}
.btn-primary {
background: var(--color-primary); /* #2563eb */
color: var(--color-text-inverse); /* White */
}
.btn-primary:hover {
background: var(--color-primary-dark); /* #1d4ed8 */
}
```
### Form Inputs
```css
input, textarea, select {
background: var(--color-background); /* #ffffff */
border: 1px solid var(--color-border); /* #e5e7eb */
border-radius: var(--radius-md); /* 8px */
padding: var(--space-3) var(--space-4); /* 12px × 16px */
font-size: var(--font-size-base); /* 16px */
}
input:focus {
border-color: var(--color-primary); /* #2563eb */
box-shadow: 0 0 0 3px var(--color-primary-lighter);
}
```
### Cards/Widgets
```css
.widget {
background: var(--color-surface-elevated); /* #ffffff */
border: 1px solid var(--color-border); /* #e5e7eb */
border-radius: var(--radius-lg); /* 12px */
box-shadow: var(--shadow-sm); /* Subtle depth */
padding: var(--space-6); /* 24px */
}
.widget:hover {
box-shadow: var(--shadow-base); /* More elevation */
transform: translateY(-1px); /* Slight lift */
}
```
### Alerts
```css
.alert {
border-radius: var(--radius-md); /* 8px */
padding: var(--space-4) var(--space-6); /* 16px × 24px */
border-left: 4px solid var(--color-success);
}
```
---
## 🌙 Dark Mode
All colors automatically switch when `prefers-color-scheme: dark` is detected:
```css
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0f172a; /* Deep navy */
--color-surface: #1e293b; /* Slate */
--color-text-primary: #f1f5f9; /* Near white */
/* ... other dark mode overrides ... */
}
}
```
**Test in Browser DevTools:**
1. F12 (Open DevTools)
2. Ctrl+Shift+P (Search in Chrome)
3. Type "rendering"
4. Toggle "Prefers color scheme" to dark
5. Page updates instantly
---
## ♿ Accessibility
### Color Contrast
```
Element Ratio WCAG Level
Primary text on white 7:1 AAA
Secondary text on white 4.5:1 AA
Links on white 5:1 AA
Buttons 4.5:1 AA
```
### Focus Indicators
```css
*:focus-visible {
outline: 2px solid var(--color-primary); /* #2563eb */
outline-offset: 2px;
}
```
### Keyboard Navigation
- All interactive elements are tab-accessible
- Logical tab order maintained
- No keyboard traps
- Focus always visible
---
## 🔧 Using CSS Variables
### Override in Your Code
```css
/* In your custom CSS, you can override tokens: */
:root {
--color-primary: #006bb3; /* Your brand blue */
--font-size-base: 18px; /* Larger default text */
}
/* All components using tokens will update automatically */
```
### Reference in Components
```css
.my-component {
background: var(--color-surface);
color: var(--color-text-primary);
padding: var(--space-4);
border-radius: var(--radius-md);
box-shadow: var(--shadow-base);
}
```
### With Fallbacks (IE11 compat, optional)
```css
.my-component {
background: #f9fafb; /* Fallback */
background: var(--color-surface); /* Modern */
color: #1f2937; /* Fallback */
color: var(--color-text-primary); /* Modern */
}
```
---
## 📊 Quick Lookup Table
| Need | Variable | Value |
|------|----------|-------|
| Brand color | `--color-primary` | #2563eb |
| Button padding | `--space-3` + `--space-4` | 12px × 16px |
| Card shadow | `--shadow-base` | 4px 6px -1px rgba... |
| Body text | `--font-size-base` | 16px |
| Heading | `--font-size-2xl` | 24px |
| Default spacing | `--space-6` | 24px |
| Card radius | `--radius-lg` | 12px |
| Focus outline | `--color-primary` | #2563eb |
| Dark background | --color-surface (dark mode) | #1e293b |
---
## 🎓 Pro Tips
1. **Always use variables** — Don't hardcode colors or sizes
2. **Spacing is 8px-based** — Use `--space-*` for consistency
3. **Test dark mode** — Use DevTools (see above)
4. **Mobile-first queries** — Styles default to mobile, expand on larger screens
5. **Focus states matter** — Never remove `outline` without adding alternative
6. **Semantic colors** — Use `--color-surface` instead of hardcoding #ffffff
---
**Last Updated**: February 2026
**Theme Version**: 1.0.0 (Modern)
**Xibo Compatibility**: 3.x+

View File

@@ -1,307 +0,0 @@
# Implementation Summary: OTS Signs Xibo Theme Redesign
## ✅ Complete Implementation
Your custom Xibo CMS theme has been fully redesigned and modernized to match the screenshots you provided. All major views and components have been replaced with a contemporary dark-themed UI.
---
## 📋 What Was Changed
### **View Files (5 files updated)**
| File | Changes |
|------|---------|
| `authed.twig` | Modern shell layout with fixed sidebar (250px) + main area. SVG icons in topbar, responsive hamburger menu, user avatar dropdown |
| `authed-sidebar.twig` | Reorganized navigation with section dividers, SVG icons for all menu items, user profile card at bottom |
| `dashboard.twig` | 3-column KPI card grid (Displays, Schedules, Users), status panels, quick action grid with 3 action cards |
| `displays.twig` | Two-column layout: left folder tree with 6 folder items, right content with search bar, stat boxes, modern data table |
| `media.twig` | Two-column layout: left folder tree, right media grid with 4 sample images, storage stats, media type badges |
### **CSS Styling (override.css - ~1,050 lines)**
**Dark theme colors:**
- Background: `#0f172a` (dark navy)
- Surface: `#1e293b` (slate)
- Elevated: `#334155` (darker slate)
- Primary: `#3b82f6` (bright blue)
- Text: `#f1f5f9` (off-white)
**Component system:**
- ✅ Sidebar (fixed, collapsible on mobile)
- ✅ Topbar (search, notifications, user menu)
- ✅ KPI cards (3-column grid, hover effects)
- ✅ Badges (success, danger, info, secondary)
- ✅ Panels (full-width and half-width)
- ✅ Tables (striped, hover states)
- ✅ Media grid (3-column, responsive)
- ✅ Buttons (primary, outline, small, ghost)
- ✅ Forms (search input styling)
- ✅ Responsive layout (768px breakpoint)
### **JavaScript (theme.js - ~120 lines)**
- Sidebar toggle on mobile
- Dropdown menu interactions
- Search focus states
- Folder item selection feedback
- Mobile viewport detection
- Click-outside-to-close menus
---
## 🎯 Key Visual Changes
### **Before → After**
| Element | Before | After |
|---------|--------|-------|
| **Background** | Light white | Dark navy (#0f172a) |
| **Sidebar** | Icon emoji (📊🖥📁) | SVG icons + proper hierarchy |
| **Dashboard KPI** | Simple text cards | Large numbered cards with gradients |
| **Tables** | Basic Bootstrap | Modern dark tables with hover states |
| **Buttons** | Basic styling | Modern gradient primary, outline variants |
| **Media Items** | Text list | Image thumbnail grid with badges |
| **Navigation** | Flat list | Organized sections with dividers |
---
## 📁 Files Modified
```
custom/otssignange/
├── config.php ..................... (unchanged - already correct)
├── css/
│ ├── override.css ............... ✅ REPLACED (1,050 lines dark theme)
│ ├── client.css ................. (unchanged)
│ └── html-preview.css ........... (unchanged)
├── js/
│ └── theme.js ................... ✅ UPDATED (interactivity)
└── views/
├── authed.twig ................ ✅ REPLACED (shell + topbar)
├── authed-sidebar.twig ........ ✅ REPLACED (nav sidebar)
├── dashboard.twig ............. ✅ REPLACED (KPI cards)
├── displays.twig .............. ✅ REPLACED (two-column)
├── media.twig ................. ✅ REPLACED (media grid)
├── index.html ................. (unchanged)
└── layouts/ ................... (inherited from Xibo)
```
---
## 🚀 Next Steps
### 1. **Deploy to Xibo CMS**
If you have Xibo installed locally:
```bash
# Copy theme to Xibo installation
cp -r /path/to/otssignstheme/custom/otssignange /path/to/xibo-cms/web/theme/custom/
# Clear cache in Xibo admin UI:
# Settings → Maintenance → Purge Cache
```
### 2. **Enable Theme in Xibo**
1. Log in to Xibo CMS admin
2. Go to **Settings → Preferences → Themes**
3. Select **"OTS Signs"** from dropdown
4. Click **Save**
5. Refresh browser
### 3. **Test Pages**
After enabling, verify these pages render correctly:
- [ ] **Dashboard** - KPI cards should display (3 columns)
- [ ] **Displays** - Folder tree on left, table on right
- [ ] **Media Library** - Folder tree, image grid with thumbnails
- [ ] **Sidebar** - Toggle on mobile (<768px)
- [ ] **Topbar** - Search, notifications, user menu
- [ ] **Responsive** - Test on mobile/tablet view
### 4. **Customize (Optional)**
Edit `/custom/otssignange/css/override.css`:
**Change primary color (line ~19):**
```css
--color-primary: #3b82f6; /* Change to #8b5cf6 for purple, etc. */
```
**Change sidebar width (line ~58):**
```css
width: 250px; /* Change to 280px, 300px, etc. */
```
**Add company logo (views/authed-sidebar.twig, line ~7):**
Replace `<span class="brand-icon">🎯</span>` with:
```twig
<img src="{{ baseUrl }}/theme/custom/otssignange/img/logo.png" alt="{{ app_name }}" style="width: 28px;" />
```
---
## 🎨 Design Highlights
### **Color Palette**
```
Primary Blue: #3b82f6 (accents, buttons, hover states)
Success Green: #10b981 (online status badges)
Danger Red: #ef4444 (offline status, alerts)
Warning Orange: #f59e0b (warnings)
Info Cyan: #0ea5e9 (information)
Background: #0f172a (main background)
Surface: #1e293b (cards, panels)
Text Primary: #f1f5f9 (headings, main text)
Text Secondary: #cbd5e1 (descriptions, labels)
```
### **Typography**
- Font: System fonts (-apple-system, BlinkMacSystemFont, Segoe UI, Roboto)
- Sizes: 12px (xs) → 36px (4xl)
- Weights: 400 (normal) → 700 (bold)
- Line heights: 1.25 (tight) → 2 (loose)
### **Spacing**
- 8px base unit
- Padding: 8px, 12px, 16px, 20px, 24px, 32px
- Gaps: 12px, 16px, 20px, 24px, 32px
- Margins: Based on spacing scale
### **Rounded Corners**
- Buttons/inputs: 6px
- Cards: 8px
- Badges: 4px
- Full: 9999px (circles)
### **Shadows**
- Hover cards: `0 4px 12px rgba(0, 0, 0, 0.15)`
- Dropdowns: `0 20px 25px -5px rgba(0, 0, 0, 0.1)`
- Large modals: `0 25px 50px -12px rgba(0, 0, 0, 0.25)`
---
## 🔧 Technical Details
### **CSS Architecture**
- **Design tokens:** 50+ CSS variables
- **Component system:** Sidebar, topbar, KPI, panel, badge, button, table, media
- **Responsive:** Mobile-first, 768px breakpoint
- **Accessibility:** Proper focus states, contrast ratios (WCAG AA)
### **JavaScript Features**
- ES6 IIFE module pattern
- Event delegation
- localStorage for state
- Mobile viewport detection
- No external dependencies
### **Browser Support**
- ✅ Chrome/Edge (latest 2 versions)
- ✅ Firefox (latest 2 versions)
- ✅ Safari (latest 2 versions)
- ❌ IE11 (CSS Grid not supported)
---
## 📊 Size & Performance
- **CSS:** ~8 KB (override.css)
- **JavaScript:** ~3 KB (theme.js)
- **Total impact:** ~11 KB additional
- **Load time:** <500ms on typical connection
- **Lighthouse:** 85+ score (with optimized images)
---
## 🐛 Troubleshooting
### Sidebar toggle not working
→ Check browser console (F12) for JavaScript errors
→ Ensure `theme.js` is loading from: `/theme/custom/otssignange/js/theme.js`
### Dark theme not applying
→ Clear Xibo cache: Settings → Maintenance → Purge Cache
→ Clear browser cache: Ctrl+Shift+Delete (Windows) / Cmd+Shift+Delete (Mac)
### Images in media grid not showing
→ Check image URLs are accessible
→ Verify image permissions (644)
→ Test with different image formats (JPEG, PNG, GIF)
### Search bar styling broken
→ Verify CSS file loaded: check Network tab (F12)
→ Check CSS file size (~8 KB)
→ Look for parse errors in Console (F12)
---
## 📚 Documentation
Complete documentation available in:
- **[THEME_IMPLEMENTATION.md](./THEME_IMPLEMENTATION.md)** - Full feature guide, customization, troubleshooting
- **[config.php](./custom/otssignange/config.php)** - Theme registration
- **Individual view files** - Twig comments explaining structure
---
## ✨ What Makes This Theme Great
**Pixel-perfect match** to your screenshots
**Fully responsive** from mobile to 4K
**Modern dark theme** with professional color palette
**SVG icons** for crisp appearance at any size
**Smooth animations** and transitions
**Keyboard accessible** navigation
**Mobile-optimized** sidebar and menus
**Zero external dependencies** (pure CSS/JS)
**Well-commented code** for easy maintenance
**Design token system** for quick customization
---
## 🎓 Learning Resources
**If you want to modify the theme further:**
1. **CSS Variables:** Start with `:root` block in override.css (lines 1-25)
2. **Component classes:** Follow `.ots-<name>` naming in CSS
3. **Twig syntax:** Check `views/*.twig` files for template structure
4. **SVG icons:** Edit SVG directly in Twig files or replace with icon font
5. **JavaScript:** Modify `js/theme.js` for new interactions
---
## 🎁 Deliverables Checklist
- ✅ Dashboard page redesigned (KPI cards, panels, quick actions)
- ✅ Displays page redesigned (folder tree, table, search)
- ✅ Media Library page redesigned (media grid, thumbnails)
- ✅ Sidebar navigation modernized (SVG icons, sections)
- ✅ Topbar created (search, notifications, user menu)
- ✅ Dark theme applied (colors, contrast, shadows)
- ✅ Responsive design (mobile sidebar, flexible layouts)
- ✅ Interactive components (toggles, dropdowns, focus states)
- ✅ Documentation (README, comments, customization guide)
- ✅ Zero breaking changes (Xibo integration intact)
---
## 📞 Support
If you encounter issues:
1. **Check console errors:** F12 → Console tab
2. **Review Network tab:** F12 → Network tab (all resources loading?)
3. **Test in incognito:** Browser incognito mode (clear caching)
4. **Verify file paths:** All CSS/JS paths relative to baseUrl
5. **Contact Xibo community:** https://community.xibo.org.uk/
---
**Theme implementation complete!** 🎉
Your OTS Signs theme is ready for deployment. All views match your screenshots with a modern dark interface, responsive design, and interactive components.

267
INDEX.md
View File

@@ -1,267 +0,0 @@
# OTS Signage Modern Theme - Complete Package
Welcome! Your Xibo CMS theme has been fully modernized. This file is your starting point.
## 📚 Documentation Index
Start here based on what you need:
### 🚀 First Time? Start Here
**→ [SUMMARY.md](SUMMARY.md)** — 5 min read
- High-level overview of what was implemented
- Before/after comparison
- Quick start (3 steps to deploy)
- What's next suggestions
### 🎨 Want to Customize?
**→ [CUSTOMIZATION.md](CUSTOMIZATION.md)** — Browse as needed
- 15 ready-made customization recipes
- Change colors, fonts, spacing, etc.
- Code examples for each task
- Advanced tips and tricks
### 📦 Ready to Deploy?
**→ [DEPLOYMENT.md](DEPLOYMENT.md)** — Follow step-by-step
- Pre-deployment checklist
- Installation instructions (3 methods)
- Post-deployment validation
- Troubleshooting guide
- Rollback procedure
### 📖 Full Documentation
**→ [README.md](README.md)** — Complete reference
- Feature documentation
- Design tokens reference
- File structure
- Accessibility checklist
- Browser compatibility
---
## 📁 Your Theme Structure
```
theme/ (Root)
├── SUMMARY.md ← START HERE (high-level overview)
├── README.md ← Full feature documentation
├── CUSTOMIZATION.md ← Customization recipes (15+)
├── DEPLOYMENT.md ← Deployment & troubleshooting
├── INDEX.md ← This file
└── custom/otssignange/
├── config.php (Theme configuration—unchanged)
├── css/
│ ├── override.css ✨ MODERNIZED (800 lines)
│ ├── html-preview.css ✨ UPDATED (gradient background)
│ └── client.css ✨ NEW (widget styling)
├── img/ (Logo assets)
├── layouts/ (Layout templates)
└── views/ (Twig overrides—empty, ready for custom)
```
---
## 🎯 What Was Delivered
### ✅ Design System
- **70+ CSS Variables** — Colors, typography, spacing, shadows, transitions
- **Dark Mode** — Automatic via system preference
- **Responsive Layout** — Mobile-first, 5 breakpoints
- **Accessibility** — WCAG AA color contrast, focus states, keyboard nav
### ✅ Modern Components
- Header/navbar with brand color
- Sidebar navigation with collapse on mobile
- Card-based widgets with shadows and hover effects
- Form controls with focus rings
- Button variants (primary, secondary, success, danger)
- Alerts/notifications (4 status types)
- Tables with hover states
### ✅ Documentation
- **4 comprehensive guides** (README, Customization, Deployment, Summary)
- **15+ customization recipes** with code examples
- **Complete API reference** for CSS variables
- **Deployment checklist** with validation steps
- **Troubleshooting guide** for common issues
---
## 🚀 Quick Start (3 Steps)
### Step 1: Review Changes
```bash
# Look at what changed
ls -la custom/otssignange/css/
```
Output: `override.css` (800 lines), `html-preview.css` (updated), `client.css` (new)
### Step 2: Backup & Deploy
```bash
# Backup original
cp custom/otssignange/css/override.css custom/otssignange/css/override.css.backup
# Copy to your Xibo installation
cp custom/otssignange/css/* /path/to/xibo/web/theme/custom/otssignange/css/
```
### Step 3: Test
1. Hard refresh browser: Ctrl+Shift+R (or Cmd+Shift+R on Mac)
2. Log into Xibo CMS
3. Verify header, sidebar, and widgets show new styling
4. Test on mobile: Resize to <640px or use device
✅ Done! Your CMS looks modern now.
---
## 🎨 Popular Customizations
Want to make it your own? Here are 3 easiest changes:
### 1. Change Brand Color (5 min)
Edit `custom/otssignange/css/override.css`:
```css
:root {
--color-primary: #006bb3; /* Your color here */
--color-primary-dark: #004c80; /* Darker version */
--color-primary-light: #1a9ad1; /* Lighter version */
}
```
### 2. Update Logo (2 min)
Replace these files in `custom/otssignange/img/`:
- `xibologo.png` (header logo)
- `192x192.png` (app icon)
- `512x512.png` (splash icon)
### 3. Change Font (5 min)
Edit `custom/otssignange/css/override.css`:
```css
:root {
--font-family-base: "Your Font", sans-serif;
}
```
**→ See [CUSTOMIZATION.md](CUSTOMIZATION.md) for 12+ more recipes!**
---
## 📊 What's Different
| Feature | Before | After |
|---------|--------|-------|
| CSS Architecture | Empty hooks | Full token system |
| Colors | Hardcoded | 70+ CSS variables |
| Dark Mode | ❌ None | ✅ Full system support |
| Responsive | Basic | ✅ Mobile-first |
| Components | Minimal | ✅ Complete design system |
| Accessibility | Basic | ✅ WCAG AA compliant |
| Documentation | Minimal | ✅ 4 complete guides |
---
## 🔧 Technical Details
### Files Modified/Created
- **override.css** — Rewritten (50 → 800 lines)
- **html-preview.css** — Updated with gradient
- **client.css** — New file for widget styling
### CSS Features Used
- CSS Variables (Custom Properties)
- Media Queries (responsive, dark mode)
- Flexbox & Grid layouts
- Focus-visible for accessibility
- prefers-color-scheme for dark mode
### Browser Support
✅ All modern browsers (Chrome, Firefox, Safari, Edge, Mobile)
⚠️ IE 11 and below: Not supported (CSS variables required)
---
## 📋 Next Steps
### Today: Deploy & Test
1. Read [SUMMARY.md](SUMMARY.md) (5 min overview)
2. Follow [DEPLOYMENT.md](DEPLOYMENT.md) (step-by-step)
3. Test on mobile and desktop
### This Week: Customize
1. Browse [CUSTOMIZATION.md](CUSTOMIZATION.md) recipes
2. Try 12 customizations
3. Share with your team
### This Month: Enhance
1. Add SVG icons to `custom/otssignange/img/`
2. Create Twig view overrides for advanced layout changes
3. Implement user-controlled dark mode toggle
---
## 💬 FAQ
**Q: Will this work with my Xibo version?**
A: Yes, tested on Xibo 3.x+. CSS-first approach means fewer compatibility issues.
**Q: Do I need to modify any PHP code?**
A: No. All changes are CSS-based. `config.php` is unchanged.
**Q: Can I still upgrade Xibo?**
A: Yes! CSS overrides are upgrade-safe. Just re-deploy the theme files after upgrades.
**Q: How do I test dark mode?**
A: Open DevTools (F12) → Rendering → Prefers color scheme: dark
**Q: What if something breaks?**
A: See [DEPLOYMENT.md](DEPLOYMENT.md) Rollback section. Takes 2 minutes.
---
## 📞 Support Resources
- **Customization Help**: [CUSTOMIZATION.md](CUSTOMIZATION.md) — 15+ recipes
- **Deployment Help**: [DEPLOYMENT.md](DEPLOYMENT.md) — Troubleshooting guide
- **Feature Reference**: [README.md](README.md) — Complete documentation
- **Xibo Official Docs**: [account.xibosignage.com/docs/](https://account.xibosignage.com/docs/)
---
## ✅ Quality Checklist
- [x] CSS syntax validated (no errors)
- [x] Design tokens comprehensive (70+ variables)
- [x] Dark mode fully working
- [x] Responsive on all devices
- [x] WCAG AA accessible
- [x] Documentation complete
- [x] Customization examples provided
- [x] Deployment steps clear
- [x] Rollback procedure documented
- [x] Production ready
---
## 📝 Version & License
- **Theme**: OTS Signage (Modern)
- **Version**: 1.0.0
- **Status**: ✅ Production Ready
- **License**: AGPLv3 (per Xibo requirements)
- **Compatibility**: Xibo CMS 3.x+
---
## 🎉 You're All Set!
Your Xibo CMS now has a modern, professional theme with a complete design system.
**Next action**: Read [SUMMARY.md](SUMMARY.md) (5 min) → Deploy (10 min) → Celebrate! 🚀
---
**Questions?** Check the guides above. Most answers are in [CUSTOMIZATION.md](CUSTOMIZATION.md) or [DEPLOYMENT.md](DEPLOYMENT.md).
**Ready to go live?** See [DEPLOYMENT.md](DEPLOYMENT.md) for step-by-step instructions.
**Want to customize?** See [CUSTOMIZATION.md](CUSTOMIZATION.md) for 15+ ready-made recipes.

View File

@@ -1,262 +0,0 @@
# OTS Signs Xibo Theme - Quick Reference Card
## 🚀 Quick Start (2 minutes)
### Step 1: Deploy Theme
```bash
# Copy to your Xibo installation (if you have it)
cp -r ~/dev/otssignstheme/custom/otssignange /path/to/xibo-cms/web/theme/custom/
```
### Step 2: Enable in Xibo
1. Login to Xibo CMS
2. Settings → Preferences → Themes
3. Select "OTS Signs"
4. Click Save
5. Refresh page
### Step 3: Verify
- Dashboard shows 3 KPI cards (Displays, Schedules, Users)
- Sidebar has organized menu sections
- Dark theme applied (navy background)
- Mobile sidebar toggle works on narrow screens
---
## 📂 File Map
| Path | Size | Purpose |
|------|------|---------|
| `css/override.css` | 20 KB | Dark theme + all components |
| `js/theme.js` | 4.6 KB | Sidebar toggle, dropdowns |
| `views/authed.twig` | 3.4 KB | Main layout shell |
| `views/authed-sidebar.twig` | 5.9 KB | Left navigation sidebar |
| `views/dashboard.twig` | 4.9 KB | Dashboard page |
| `views/displays.twig` | 5.1 KB | Displays management |
| `views/media.twig` | 5.2 KB | Media library |
---
## 🎨 Color Reference
```
--color-primary: #3b82f6 Blue (buttons, accents)
--color-background: #0f172a Navy (main background)
--color-surface: #1e293b Slate (cards)
--color-surface-elevated: #334155 Darker slate (headers)
--color-text-primary: #f1f5f9 Off-white (text)
--color-text-secondary: #cbd5e1 Gray (labels)
--color-success: #10b981 Green (online status)
--color-danger: #ef4444 Red (offline status)
```
---
## 🔧 Quick Customization
### Change Primary Color
**File:** `css/override.css` (line ~19)
```css
--color-primary: #3b82f6; /* Change me! */
--color-primary-dark: #1d4ed8;
--color-primary-light: #60a5fa;
```
**Suggested colors:**
- Purple: `#8b5cf6` / `#7c3aed` / `#6d28d9`
- Red: `#ef4444` / `#dc2626` / `#b91c1c`
- Green: `#10b981` / `#059669` / `#047857`
- Orange: `#f97316` / `#ea580c` / `#c2410c`
### Change Sidebar Width
**File:** `css/override.css` (line ~58)
```css
.ots-sidebar {
width: 250px; /* Change to 280px, 300px, etc. */
}
.ots-main {
margin-left: 250px; /* Must match sidebar width */
}
```
### Add Company Logo
**File:** `views/authed-sidebar.twig` (line ~7)
```twig
<!-- Replace: -->
<span class="brand-icon">🎯</span>
<!-- With: -->
<img src="{{ baseUrl }}/theme/custom/otssignange/img/logo.png"
alt="{{ app_name }}" style="width: 28px; height: 28px;" />
```
---
## ✅ Component Classes
### Layout
- `.ots-shell` - Main wrapper
- `.ots-sidebar` - Left navigation
- `.ots-topbar` - Top header
- `.ots-content` - Main content area
- `.ots-footer` - Footer
### Dashboard
- `.kpi-section` - KPI cards container
- `.kpi-card` - Single KPI card
- `.dashboard-panels` - Panel grid
- `.panel` - Card component
- `.action-cards` - Quick actions grid
### Displays/Media
- `.two-column-layout` - Layout wrapper
- `.left-panel` - Sidebar panel
- `.content-panel` - Main content
- `.folder-tree` - Folder list
- `.media-grid` - Image grid
### Common
- `.btn`, `.btn-primary`, `.btn-outline`, `.btn-sm`
- `.badge`, `.badge-success`, `.badge-danger`
- `.table`, `.table-striped`
- `.text-muted`, `.text-xs`
---
## 🐛 Common Issues
| Issue | Solution |
|-------|----------|
| Dark theme not showing | Clear cache: Settings → Maintenance → Purge Cache |
| Sidebar toggle not working | Check browser console (F12) for errors |
| Images not showing in media grid | Verify image URLs are accessible, check permissions |
| Mobile sidebar stuck off-screen | Test in new browser tab, clear localStorage |
| CSS not loading | Check file exists at `web/theme/custom/otssignange/css/override.css` |
---
## 📱 Responsive Breakpoints
```css
Mobile: max-width: 640px
Tablet: 641px - 768px
Desktop: 769px+
Key behavior:
- Sidebar: hidden/drawer on mobile, fixed on desktop
- Topbar: flex-column on mobile, flex-row on desktop
- Grids: 1 column on mobile, 2+ columns on desktop
```
---
## 🎯 CSS Variables Quick Edit
**File:** `css/override.css` top section
Change any of these to customize the entire theme:
```css
--color-primary: #3b82f6; /* Primary brand color */
--color-background: #0f172a; /* Main background */
--color-surface: #1e293b; /* Card background */
--color-border: #475569; /* Divider lines */
--color-text-primary: #f1f5f9; /* Main text */
--color-text-secondary: #cbd5e1; /* Secondary text */
--color-text-tertiary: #94a3b8; /* Muted text */
```
---
## 🔗 Important Paths
Relative to Xibo root:
```
/web/theme/custom/otssignange/
├── config.php
├── css/override.css ← Main styling
├── js/theme.js ← Interactions
└── views/
├── authed.twig ← Main shell
├── authed-sidebar.twig ← Sidebar nav
├── dashboard.twig
├── displays.twig
└── media.twig
```
---
## 📊 File Sizes
- `override.css`: 20 KB
- `theme.js`: 4.6 KB
- `authed.twig`: 3.4 KB
- `authed-sidebar.twig`: 5.9 KB
- `dashboard.twig`: 4.9 KB
- `displays.twig`: 5.1 KB
- `media.twig`: 5.2 KB
**Total theme size:** ~49 KB
---
## 🎓 Code Examples
### Use in Twig
```twig
<!-- KPI Card -->
<div class="kpi-card">
<div class="kpi-header">
<h3 class="kpi-label">Displays</h3>
</div>
<div class="kpi-body">
<div class="kpi-number">{{ count }}</div>
</div>
</div>
<!-- Button -->
<a href="{{ baseUrl }}/display" class="btn btn-primary">Add Display</a>
<!-- Badge -->
<span class="badge badge-success">Online</span>
```
### CSS Variables in Custom Styles
```css
.my-component {
background: var(--color-surface);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4);
transition: all var(--transition-base);
}
```
---
## 🚨 Before Modifying
1. **Backup original:** `cp override.css override.css.backup`
2. **Test locally:** Use browser DevTools to test changes
3. **Clear cache:** After any CSS/JS change, purge Xibo cache
4. **Check mobile:** Test responsive changes at 375px width
---
## 📞 Need Help?
Check these files in order:
1. `IMPLEMENTATION_COMPLETE.md` - What was changed
2. `THEME_IMPLEMENTATION.md` - Full documentation
3. `views/authed.twig` - Template structure
4. `css/override.css` - Component styling
5. `js/theme.js` - Interactivity
---
**Last Updated:** February 4, 2026
**Theme Version:** 1.0.0
**Xibo Compatibility:** v4.0+

266
README.md
View File

@@ -1,266 +0,0 @@
# OTS Signage - Modern Xibo CMS Theme
A modernized theme for Xibo CMS that brings contemporary UI/UX patterns, improved accessibility, and a comprehensive design token system.
## What's New
### 🎨 Design Tokens System
- **Color palette**: 10 semantic colors + 9-step gray scale with light/dark mode support
- **Typography**: System font stack, 8-step type scale, 5 font weights
- **Spacing**: 8px-based scale (080px) for consistent margins and padding
- **Elevation**: Shadow system with 6 levels for depth perception
- **Radius**: Border radius scale from sharp to full-rounded
- **Transitions**: Predefined animation durations for consistency
### 🌙 Dark Mode Support
- Automatic dark mode via `prefers-color-scheme` media query
- Token overrides for dark theme built-in
- No additional JavaScript required for system preference
### 📱 Responsive Layout
- Mobile-first responsive grid system
- Sidebar collapses into hamburger on screens ≤768px
- Flexible widget containers with auto-fit grid
- Optimized breakpoints (sm: 640px, md: 768px, lg: 1024px, xl: 1280px)
### 🎯 Modern Components
- **Cards**: Widgets styled as elevated cards with shadows and hover effects
- **Typography**: Improved hierarchy with modern font sizing
- **Forms**: Refined input styling with focus ring system
- **Buttons**: Consistent button styles with proper color variants
- **Alerts**: Color-coded alerts (success, danger, warning, info)
- **Tables**: Clean table styling with hover states
### ♿ Accessibility Improvements
- WCAG AA compliant color contrasts (verified on primary palette)
- Focus-visible outlines for keyboard navigation
- Semantic HTML structure preserved
- Proper heading hierarchy support
- Form labels and ARIA attributes maintained
### 📊 Widget Styling
- Card-based widget design with consistent shadows
- Hover lift effect for interactivity feedback
- Proper spacing and padding via tokens
- Title bars with semantic color distinction
- Dashboard grid with responsive columns
## File Structure
```
custom/otssignange/
├── config.php # Theme configuration (unchanged)
├── css/
│ ├── override.css # Main theme styles + design tokens (UPDATED)
│ ├── html-preview.css # Preview splash screen (UPDATED)
│ └── client.css # Widget embedding styles (NEW)
├── img/
│ ├── favicon.ico
│ ├── 192x192.png
│ ├── 512x512.png
│ └── xibologo.png
├── layouts/
│ └── default-layout.zip
└── views/
└── index.html # Empty - ready for Twig overrides if needed
```
## CSS Variables Reference
### Colors
```css
/* Brand Colors */
--color-primary: #2563eb
--color-primary-dark: #1d4ed8
--color-primary-light: #3b82f6
--color-secondary: #7c3aed
/* Semantic Colors */
--color-success: #10b981
--color-warning: #f59e0b
--color-danger: #ef4444
--color-info: #0ea5e9
/* Grays (50900) */
--color-gray-50, 100, 200, 300, 400, 500, 600, 700, 800, 900
/* Semantic Semantic */
--color-background: #ffffff (dark: #0f172a)
--color-surface: #f9fafb (dark: #1e293b)
--color-text-primary: #1f2937 (dark: #f1f5f9)
--color-text-secondary: #6b7280 (dark: #cbd5e1)
--color-border: #e5e7eb (dark: #475569)
```
### Typography
```css
--font-family-base: System font stack
--font-size-xs to 4xl: 0.75rem 2.25rem
--font-weight-normal/medium/semibold/bold: 400700
--line-height-tight/snug/normal/relaxed/loose: 1.252
```
### Spacing (8px base)
```css
--space-1 through --space-20: 0.25rem 5rem
```
### Elevation & Borders
```css
--shadow-none/xs/sm/base/md/lg/xl
--radius-none/sm/base/md/lg/xl/2xl/full
--transition-fast/base/slow: 150ms300ms
```
## Customization Guide
### Change Brand Color
Edit `override.css` root selector:
```css
:root {
--color-primary: #your-color;
--color-primary-dark: #darker-shade;
--color-primary-light: #lighter-shade;
}
```
### Update Typography
Modify font family and scale:
```css
:root {
--font-family-base: "Your Font", sans-serif;
--font-size-base: 1.125rem; /* Increase base from 16px to 18px */
}
```
### Adjust Spacing
Change the base spacing multiplier (affects all --space-* variables):
```css
/* If you prefer tighter spacing, reduce proportionally */
--space-4: 0.75rem; /* was 1rem */
--space-6: 1.125rem; /* was 1.5rem */
```
### Implement Dark Mode Toggle (Optional)
If you want a user-controlled toggle instead of system preference:
1. Add a button to toggle theme in a Twig view override
2. Store preference in localStorage
3. Add to `override.css`:
```css
[data-theme="dark"] {
--color-background: #0f172a;
--color-surface: #1e293b;
/* etc. */
}
```
## Files Modified
### `css/override.css`
**Changes:**
- Replaced empty CSS hooks with comprehensive design token system
- Added global styles using tokens
- Implemented responsive header/sidebar with mobile hamburger
- Added widget card styling with elevation effects
- Included form, button, table, and alert component styles
- Added responsive grid utilities and spacing classes
**Size:** ~800 lines (was ~50 lines)
### `css/html-preview.css`
**Changes:**
- Updated splash screen with gradient background matching brand color
- Added preview widget styling for consistency
- Improved visual fidelity with shadows and spacing
### `css/client.css` (NEW)
**Purpose:**
- Styles HTML/embedded widgets on displays
- Uses mirrored design tokens for consistency
- Includes typography, form, button, and alert styles
- Supports dark mode with prefers-color-scheme
## Deployment Notes
### Before Deploying to Production
1. **Test in your Xibo CMS instance:**
- Log in and verify admin UI appearance
- Check sidebar navigation, widgets, and forms
- Test on mobile (resize browser or use device)
- Verify color contrast with a tool like WAVE or aXe
2. **Verify Asset Paths:**
- Ensure `preview/img/xibologo.png` is accessible
- Check that logo files in `img/` are served correctly
- Validate CSS paths resolve in your installation
3. **Browser Compatibility:**
- CSS variables supported in all modern browsers (Chrome 49+, Firefox 31+, Safari 9.1+, Edge 15+)
- For legacy browser support, add CSS fallbacks (not included)
4. **Backup:**
- Keep a backup of original `override.css` before deployment
### Installation
1. Copy the updated theme files to `/web/theme/custom/otssignange/`
2. Clear Xibo CMS cache if applicable
3. Reload the CMS in your browser
4. No database changes or CMS restart required
### Rollback
If issues occur, revert to backup:
```bash
cp override.css.backup override.css
# Refresh browser cache
```
## Responsive Breakpoints
| Breakpoint | Width | Use Case |
|-----------|-------|----------|
| **Mobile** | < 640px | Single column layout |
| **Tablet** | 640768px | Sidebar collapse |
| **Desktop** | 7681024px | 23 column grids |
| **Large** | 10241280px | Multi-column dashboards |
| **Ultra** | ≥ 1280px | Full-width content |
## Accessibility Checklist
- [x] WCAG AA color contrast (4.5:1 text, 3:1 components)
- [x] Focus visible outlines (2px solid primary color)
- [x] Keyboard navigation preserved
- [x] Semantic HTML maintained
- [x] Form labels and ARIA attributes respected
- [x] No color-only information conveyed
- [x] Sufficient touch target sizes (≥44px)
## Future Enhancements
Suggested follow-ups:
- Create Twig view overrides for `authed.twig` and `authed-sidebar.twig` for DOM-level layout improvements
- Add SVG icon sprite for consistent iconography across CMS
- Implement animated transitions for sidebar collapse
- Add data visualization component styles (charts, graphs)
- Create documentation portal in Xibo for custom branding
## Support & Questions
For documentation on Xibo CMS theming:
- [Xibo Developer Docs](https://account.xibosignage.com/docs/developer/)
- Theme config reference: `config.php`
- Available Twig overrides: Check `/web/theme/default/views/` in your Xibo installation
## License
This theme extends Xibo CMS and is subject to AGPLv3 license per Xibo requirements.
Xibo is © 20062021 Xibo Signage Ltd.
---
**Theme Version:** 1.0.0 (Modern)
**Last Updated:** February 2026
**Xibo CMS Compatibility:** 3.x and above

View File

@@ -1,260 +0,0 @@
# Implementation Summary
## What Was Done
Your Xibo CMS OTS Signage theme has been fully modernized with a comprehensive design system. Here's what's been delivered:
### 📁 Files Created/Updated
#### 1. **`css/override.css`** (Main Theme File)
- **Status**: ✅ Complete rewrite
- **Lines**: ~800 (was ~50)
- **Contains**:
- 70+ CSS variables defining colors, typography, spacing, shadows, transitions
- Dark mode support via `prefers-color-scheme`
- Global typography styles with proper hierarchy
- Modern header/navbar styling
- Responsive sidebar with mobile hamburger menu
- Card-based widget styling with elevation effects
- Form controls with focus rings
- Button component variants (primary, secondary, success, danger)
- Alert/notification styling
- Table styling with hover effects
- Responsive grid utilities
- Accessibility features (focus-visible, color contrast)
#### 2. **`css/html-preview.css`** (Preview Styling)
- **Status**: ✅ Updated
- **Changes**:
- Gradient background matching brand color
- Preview widget component styling
- Consistent with main theme tokens
#### 3. **`css/client.css`** (Widget Styling)
- **Status**: ✅ New file created
- **Purpose**: Styles HTML/embedded widgets on displays
- **Includes**: Typography, forms, buttons, tables, alerts, dark mode support
#### 4. **`README.md`** (Full Documentation)
- **Status**: ✅ Comprehensive guide created
- **Contents**:
- Feature overview (design tokens, dark mode, responsive, accessibility)
- File structure reference
- CSS variable reference guide
- Customization basics
- Deployment notes
- Responsive breakpoints
- Accessibility compliance checklist
#### 5. **`CUSTOMIZATION.md`** (Recipes & Cookbook)
- **Status**: ✅ 15 customization recipes provided
- **Examples**:
- Change brand colors
- Update typography
- Adjust spacing
- Implement dark mode toggle
- Create custom alert styles
- And 10 more...
#### 6. **`DEPLOYMENT.md`** (Quick Start & Checklist)
- **Status**: ✅ Ready for production
- **Includes**:
- Pre-deployment checklist
- Step-by-step installation (3 methods)
- Post-deployment validation
- Rollback procedures
- Browser support matrix
- Troubleshooting guide
---
## 🎯 Key Features Implemented
### Design System
| Element | Details |
|---------|---------|
| **Color Palette** | 10 semantic colors + 9-step gray scale |
| **Typography** | System font stack, 8-step type scale, 5 weights |
| **Spacing** | 8px-based scale (13 units from 4px to 80px) |
| **Shadows** | 6-level elevation system for depth |
| **Radius** | 8 border radius options (sharp to full-round) |
| **Transitions** | 3 predefined animation speeds |
### Responsive Design
- Mobile-first approach
- Sidebar collapses into hamburger menu on screens ≤768px
- Flexible grid layouts with auto-fit
- 5 breakpoints (sm, md, lg, xl, 2xl)
### Dark Mode
- Automatic via system preference (`prefers-color-scheme: dark`)
- No JavaScript required
- All colors adjusted for readability in dark mode
### Accessibility
- WCAG AA color contrast compliance
- Focus-visible outlines for keyboard navigation
- Semantic HTML preserved
- Form labels and ARIA attributes maintained
- Proper heading hierarchy support
### Components
- Header/navbar with brand color
- Sidebar navigation with active states
- Cards with elevation and hover effects
- Forms with focus rings
- Buttons (4 color variants)
- Alerts (4 status types)
- Tables with hover states
- Modal & dialog support ready
---
## 📊 Comparison: Before vs. After
| Aspect | Before | After |
|--------|--------|-------|
| **CSS Architecture** | Empty hooks | Comprehensive token system |
| **Color System** | No tokens | 70+ CSS variables |
| **Dark Mode** | Not supported | Full system preference support |
| **Responsive** | Basic | Mobile-first with breakpoints |
| **Components** | Minimal | Complete design system |
| **Accessibility** | Baseline | WCAG AA compliant |
| **Documentation** | Minimal | 4 comprehensive guides |
| **Customization** | Limited | 15+ recipes provided |
---
## 🚀 Quick Start
### 1. Review the Changes
```bash
# Check modified files
ls -la custom/otssignange/css/
# Output:
# override.css (800 lines, modernized)
# html-preview.css (updated with gradient)
# client.css (new, for widgets)
```
### 2. Backup Current Theme
```bash
cp custom/otssignange/css/override.css custom/otssignange/css/override.css.backup
```
### 3. Deploy to Xibo
Copy the three CSS files to your Xibo installation:
```bash
cp custom/otssignange/css/* /path/to/xibo/web/theme/custom/otssignange/css/
```
### 4. Clear Cache & Test
- Hard refresh browser: Ctrl+Shift+R
- Log into Xibo CMS
- Verify header, sidebar, and widgets display with new styling
- Test on mobile: Resize browser to <640px or use device
### 5. Customize (Optional)
See [CUSTOMIZATION.md](CUSTOMIZATION.md) for 15 ready-made recipes to adjust colors, fonts, spacing, etc.
---
## 📋 What's Next?
### Immediate (Try It)
1. Deploy to your Xibo test instance
2. Verify appearance across devices
3. Test color contrast with WAVE or aXe tools
4. Customize brand colors if needed (see CUSTOMIZATION.md)
### Short Term (Enhancement)
- Add SVG icon sprite to `img/` for better iconography
- Create Twig view overrides for header/sidebar layout customization
- Implement user-controlled dark mode toggle
### Medium Term (Polish)
- Add data visualization component styles (charts, graphs)
- Create mini documentation portal within Xibo for custom branding
- Add animations/transitions for sidebar collapse, form interactions
### Long Term (Expansion)
- Style custom Xibo modules/extensions
- Create light/dark theme variants as separate CSS files
- Build theme generator tool for rapid customization
---
## 🔄 Rollback (If Needed)
If any issues occur after deployment:
```bash
# Restore backup
cp custom/otssignange/css/override.css.backup custom/otssignange/css/override.css
# Remove new file if problematic
rm custom/otssignange/css/client.css
# Clear cache and refresh
# (Clear Xibo cache or hard-refresh browser)
```
---
## 📚 Documentation Files
| File | Purpose |
|------|---------|
| **README.md** | Full feature documentation, tokens reference, accessibility checklist |
| **CUSTOMIZATION.md** | 15+ customization recipes (change colors, fonts, spacing, etc.) |
| **DEPLOYMENT.md** | Installation steps, validation checklist, troubleshooting |
| **SUMMARY.md** | This file—high-level overview |
---
## ✅ Quality Assurance
- [x] CSS syntax validated (no errors)
- [x] Design tokens comprehensive (70+ variables)
- [x] Dark mode fully implemented
- [x] Responsive breakpoints correct
- [x] Color contrast WCAG AA compliant
- [x] Accessibility features included
- [x] Documentation complete
- [x] Customization recipes provided
- [x] Deployment guide created
- [x] Rollback procedure documented
---
## 🎓 Learning Resources
- **CSS Variables**: [MDN - CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*)
- **Dark Mode**: [MDN - prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
- **Responsive Design**: [MDN - Responsive Design](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design)
- **Xibo Theming**: [Xibo Developer Docs](https://account.xibosignage.com/docs/developer/)
---
## 📝 Version Info
- **Theme Name**: OTS Signage (Modern)
- **Version**: 1.0.0
- **Status**: Production Ready
- **Last Updated**: February 2026
- **Xibo Compatibility**: 3.x and above
- **Browser Support**: All modern browsers (Chrome, Firefox, Safari, Edge, Mobile)
---
## 💡 Support
For issues or questions:
1. Check [DEPLOYMENT.md](DEPLOYMENT.md) troubleshooting section
2. Review [CUSTOMIZATION.md](CUSTOMIZATION.md) for common tasks
3. Consult [README.md](README.md) for feature details
4. Refer to [Xibo Developer Docs](https://account.xibosignage.com/docs/developer/)
---
**Status**: ✅ Implementation Complete & Ready for Deployment

View File

@@ -1,396 +0,0 @@
# OTS Signs Xibo CMS Theme - Implementation Guide
## Overview
This is a modern, dark-themed Xibo CMS theme with a professional UI redesign. The theme replaces all major views (dashboard, displays, media library) with contemporary components, a responsive sidebar navigation, and a modern topbar layout.
**Theme Name:** OTS Signs
**Theme Directory:** `custom/otssignange/`
**Target Xibo Version:** Latest stable (v4.0+)
**License:** AGPL-3.0 (Xibo)
---
## Implementation Summary
### ✅ Completed Changes
#### 1. **Views Updated**
- **authed.twig** - Modern shell with fixed sidebar + main layout, SVG icons, responsive topbar
- **authed-sidebar.twig** - Enhanced sidebar with organized nav sections, user profile card, SVG icons
- **dashboard.twig** - KPI cards, status panels, quick action grid
- **displays.twig** - Two-column layout with folder tree, modern table, stat boxes
- **media.twig** - Media grid with image previews, storage stats, folder structure
#### 2. **CSS Styling (override.css)**
- **Dark theme colors:** Navy backgrounds (#0f172a), slate surfaces (#1e293b), blue accents (#3b82f6)
- **Component system:** Sidebar, topbar, KPI cards, badges, buttons, tables, media grid
- **Responsive design:** Mobile breakpoints at 768px, flexible grid layouts
- **Transitions & effects:** Smooth hover states, focus states, elevation shadows
- **CSS variables:** Comprehensive design token system for easy customization
#### 3. **JavaScript Functionality (theme.js)**
- Sidebar toggle with mobile responsiveness
- Dropdown menu interactions (user menu)
- Search form focus states
- Page-specific interactions (folder selection, media item interaction)
- Mobile viewport detection and adaptive behavior
---
## File Structure
```
custom/otssignange/
├── config.php # Theme registration & configuration
├── css/
│ ├── client.css # HTML widget styling (mirrored design tokens)
│ ├── override.css # Main dark theme & component styles (1000+ lines)
│ └── html-preview.css # Preview mode styles
├── js/
│ └── theme.js # Interactive components & sidebar toggle
├── img/ # Placeholder for logo/icons
├── views/
│ ├── authed.twig # Main shell (sidebar + topbar + main area)
│ ├── authed-sidebar.twig # Left navigation sidebar
│ ├── dashboard.twig # Dashboard page with KPI cards
│ ├── displays.twig # Displays management page
│ ├── media.twig # Media library page
│ ├── index.html # Fallback page
│ └── layouts/ # Layout templates (inherited from Xibo core)
└── README.md # This file
```
---
## Key Features
### 🎨 **Dark Theme**
- **Colors:** Dark navy backgrounds, slate panels, bright blue primary accent
- **Contrast:** WCAG AA compliant (high contrast text)
- **Consistent:** Applied across all pages and components
### 📱 **Responsive Layout**
- **Sidebar:** Fixed on desktop, slide-in drawer on mobile (<768px)
- **Topbar:** Responsive search bar, user menu, notification button
- **Content:** Flexible grids that stack on smaller screens
- **Tables:** Horizontal scroll on mobile, proper alignment
### 🎯 **Modern Components**
- **KPI Cards:** Display key metrics (displays online, schedules, users)
- **Panels:** Two-column layouts for displays & media sections
- **Tables:** Striped rows, hover states, action menus
- **Media Grid:** Thumbnail preview cards with metadata
- **Badges:** Status indicators (Online/Offline, Success/Danger/Info)
- **Buttons:** Primary (blue), outline, small, and ghost variants
### ⚡ **Interactivity**
- Sidebar toggle on mobile
- Dropdown menus (user profile menu)
- Folder/item selection with visual feedback
- Search input focus states
- Smooth transitions (150-300ms)
---
## Installation & Deployment
### Option 1: Local Development (Xibo CMS Installed)
1. **Navigate to theme directory:**
```bash
cd /path/to/xibo-cms/web/theme/custom/
```
2. **Copy the theme folder:**
```bash
cp -r /path/to/otssignstheme/custom/otssignange ./
```
3. **Enable in Xibo Admin:**
- Go to **Settings → Preferences → Themes**
- Select "OTS Signs" from the dropdown
- Click **Save**
4. **Clear caches:**
- Go to **Settings → Maintenance → Purge Cache**
- Refresh the page
### Option 2: Package for Distribution
Create a ZIP file for sharing:
```bash
cd /path/to/otssignstheme/custom/
zip -r ots-signs-theme.zip otssignange/
```
**Distribution contents:**
- `otssignange/` - Full theme directory
- `INSTALLATION.txt` - Setup instructions
- `LICENSE` - AGPL-3.0 license
---
## Customization
### Change Primary Color
Edit `css/override.css`, line ~10:
```css
--color-primary: #3b82f6; /* Change from blue to your color */
--color-primary-dark: #1d4ed8;
--color-primary-light: #60a5fa;
```
**Hex color suggestions:**
- Purple: `#8b5cf6`
- Red: `#ef4444`
- Green: `#10b981`
- Orange: `#f97316`
### Adjust Sidebar Width
Edit `css/override.css`, line ~58:
```css
.ots-sidebar {
width: 250px; /* Change to 280px, 300px, etc. */
}
.ots-main {
margin-left: 250px; /* Must match sidebar width */
}
```
### Customize Fonts
Edit `css/override.css`:
```css
/* Base font family */
--font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
/* Monospace (for code) */
--font-family-mono: 'Monaco', monospace;
```
### Add Company Logo
1. Place logo at `img/logo.png` (recommended: 40x40px, PNG/SVG)
2. Edit `views/authed-sidebar.twig`, line ~7:
```twig
<span class="brand-icon">🎯</span> <!-- Replace emoji with: -->
<img src="{{ baseUrl }}/theme/custom/otssignange/img/logo.png" alt="{{ app_name }}" style="width: 28px; height: 28px;" />
```
---
## Browser Compatibility
- **Chrome/Edge:** ✅ Full support (latest 2 versions)
- **Firefox:** ✅ Full support (latest 2 versions)
- **Safari:** ✅ Full support (latest 2 versions)
- **IE11:** ❌ Not supported (CSS Grid, CSS Variables not available)
**CSS Features Used:**
- CSS Grid (layout)
- CSS Flexbox (alignment)
- CSS Variables (theming)
- CSS Transitions (animations)
- SVG inline (icons)
---
## Common Issues & Troubleshooting
### Issue: Sidebar not toggling on mobile
**Solution:** Ensure `theme.js` is loaded. Check browser console for errors:
```bash
Press F12 → Console tab → Look for red errors
```
### Issue: Dark theme not applying
**Solution:** Clear Xibo cache and browser cache:
1. **Xibo:** Settings → Maintenance → Purge Cache
2. **Browser:** Ctrl+Shift+Delete (Windows) or Cmd+Shift+Delete (Mac)
### Issue: Images in media grid not showing
**Solution:** Verify image URLs are accessible. Check:
1. File permissions (644 for files, 755 for directories)
2. Image format is supported (JPEG, PNG, GIF, WebP)
### Issue: Search bar styling broken
**Solution:** Ensure `override.css` is fully loaded. Check:
1. CSS file size: should be ~8-10 KB
2. No CSS parse errors in DevTools (F12 → Console)
---
## Development & Debugging
### Enable Debug Mode
Edit `config.php`:
```php
// Add at top of file
define('DEBUG', true);
```
### View Generated HTML
In browser, right-click → **Inspect Element** to see:
- DOM structure
- Applied CSS classes
- CSS rules (with file/line)
- SVG icons rendering
### Test Responsive Design
In browser DevTools (F12):
1. Click **Toggle Device Toolbar** (or Ctrl+Shift+M)
2. Select mobile device (iPhone 12, Pixel 5, etc.)
3. Test sidebar toggle, search, menus
---
## Performance Metrics
- **CSS file size:** ~8 KB (minified)
- **JS file size:** ~3 KB (minified)
- **Load time (typical):** <500ms additional
- **Lighthouse score:** 85+ (with proper images)
---
## Xibo CMS Integration Notes
### Theme Hooks (Twig Blocks)
The theme extends `base.twig` and overrides:
- `{% block head %}` - Link to custom CSS
- `{% block htmlTag %}` - Dark mode attribute
- `{% block body %}` - Custom shell structure
- `{% block header %}` - Topbar
- `{% block content %}` - Main content area
- `{% block footer %}` - Footer
- `{% block scripts %}` - Include theme.js
### Available Twig Variables
In views, you can access:
```twig
{{ baseUrl }} # Base URL of Xibo CMS
{{ app_name }} # Application name (from config.php)
{{ user.username }} # Current user's login
{{ currentDate }} # Current date/time
{{ pageTitle }} # Page title (from view)
{{ pageSubtitle }} # Optional page subtitle
```
### CSS Class Conventions
Custom classes follow BEM naming:
```
.ots-<component> # Root component
.ots-<component>__item # Child element
.ots-<component>--active # Modifier
```
---
## Future Enhancements
### Planned Features
- [ ] Light mode toggle (currently dark only)
- [ ] Custom color picker in admin
- [ ] Theme variants (compact, expanded sidebar)
- [ ] Export/import settings
- [ ] RTL (Right-to-Left) support
### Community Contribution Ideas
- Additional color schemes
- Accessibility improvements (AAA contrast)
- More page overrides (Settings, Users, Schedules)
- Keyboard navigation enhancements
- Dashboard widget system
---
## Support & License
**License:** AGPL-3.0 (inherited from Xibo CMS)
**Legal Notice:**
This theme is provided as-is for use with Xibo Digital Signage CMS. It maintains compatibility with Xibo's AGPL license. Any modifications must be shared with the community under the same license.
**Support Channels:**
- Xibo Community Forum: https://community.xibo.org.uk/
- GitHub Issues: https://github.com/xibosignage/xibo-cms/issues
---
## Credits
**Theme Created For:** OTS Signs
**Based On:** Xibo CMS v4.0+ default theme
**Design System:** Modern dark theme with blue accent
**Created:** February 2026
---
## Changelog
### v1.0.0 (Initial Release)
- Complete redesign of dashboard, displays, media views
- Dark theme with blue accent color
- Responsive sidebar & topbar
- Modern component system
- SVG icon integration
- Keyboard & mobile accessibility
- ~1000 lines of new CSS
- Interactive JavaScript components
---
## Quick Reference
### CSS Variables
```css
--color-primary: #3b82f6 /* Main brand color */
--color-background: #0f172a /* Page background */
--color-surface: #1e293b /* Card/panel background */
--color-text-primary: #f1f5f9 /* Main text */
--color-border: #475569 /* Dividers */
```
### Breakpoints
```css
Mobile: max-width: 640px
Tablet: 641px - 768px
Desktop: 769px+
```
### Key Classes
```
.ots-shell /* Main layout wrapper */
.ots-sidebar /* Left navigation */
.ots-topbar /* Top header bar */
.ots-content /* Main content area */
.kpi-section /* Dashboard KPI grid */
.panel /* Card component */
.btn /* Button */
.badge /* Status badge */
.table /* Data table */
.media-grid /* Image grid */
```
---
**For questions or issues, refer to the Xibo Community Forum or review the theme files directly.**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -131,6 +131,123 @@
});
}
/**
* Prevent Chart.js errors when chart elements are missing
*/
function initChartSafeguard() {
if (!window.Chart) return;
if (typeof window.Chart.acquireContext === 'function') {
window.Chart.acquireContext = function(item) {
if (!item) return null;
const candidate = item.length ? item[0] : item;
if (candidate && typeof candidate.getContext === 'function') {
return candidate.getContext('2d');
}
return null;
};
return;
}
if (window.Chart.prototype && typeof window.Chart.prototype.acquireContext === 'function') {
window.Chart.prototype.acquireContext = function(item) {
if (!item) return null;
const candidate = item.length ? item[0] : item;
if (candidate && typeof candidate.getContext === 'function') {
return candidate.getContext('2d');
}
return null;
};
}
}
/**
* Enhance tables: wrap in card, add per-table search box, client-side filtering
* Non-destructive: skips tables already enhanced
*/
function enhanceTables() {
const selector = '.ots-content table, .content table, .container table, .card table, table';
const tables = Array.from(document.querySelectorAll(selector));
let counter = 0;
tables.forEach(table => {
// only enhance tables that have a thead and tbody
if (!table || table.classList.contains('modern-table')) return;
if (!table.querySelector('thead') || !table.querySelector('tbody')) return;
counter += 1;
table.classList.add('modern-table');
// Build wrapper structure
const wrapper = document.createElement('div');
wrapper.className = 'modern-table-card';
const controls = document.createElement('div');
controls.className = 'table-controls';
const input = document.createElement('input');
input.type = 'search';
input.placeholder = 'Search…';
input.className = 'table-search-input';
input.setAttribute('aria-label', 'Table search');
input.style.minWidth = '180px';
controls.appendChild(input);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-wrapper';
tableWrapper.style.overflow = 'auto';
// Insert wrapper into DOM in place of the table
const parent = table.parentNode;
parent.replaceChild(wrapper, table);
wrapper.appendChild(controls);
wrapper.appendChild(tableWrapper);
tableWrapper.appendChild(table);
// Simple, light-weight search filtering for this table only
input.addEventListener('input', function (e) {
const term = (e.target.value || '').toLowerCase();
table.querySelectorAll('tbody tr').forEach(tr => {
const text = tr.textContent.toLowerCase();
tr.style.display = term === '' || text.includes(term) ? '' : 'none';
});
});
});
}
/**
* Initialize DataTables for enhanced behavior when available.
* Falls back gracefully if DataTables or jQuery are not present.
*/
function initDataTables() {
if (!window.jQuery) return;
const $ = window.jQuery;
if (!$.fn || !$.fn.dataTable) return;
$('.modern-table, table').each(function () {
try {
if (!$.fn.dataTable.isDataTable(this)) {
$(this).DataTable({
responsive: true,
lengthChange: false,
pageLength: 10,
autoWidth: false,
dom: '<"table-controls"f>rt<"table-meta"ip>',
language: { search: '' }
});
}
} catch (err) {
// If initialization fails, ignore and allow fallback enhancer
console.warn('DataTables init failed for table', this, err);
}
});
}
/**
* Initialize all features when DOM is ready
*/
@@ -139,35 +256,13 @@
initDropdowns();
initSearch();
initPageInteractions();
initDataTables();
enhanceTables();
makeResponsive();
initChartSafeguard();
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
themeBtn.addEventListener('click', function() {
const currentTheme = html.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem(STORAGE_KEYS.themeMode, newTheme);
themeBtn.setAttribute('aria-pressed', newTheme === 'dark');
});
}
/**
* Initialize page on DOM ready
*/
function init() {
initSidebarToggle();
initThemeToggle();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {

View File

@@ -1,116 +1,356 @@
{#
OTS Signage Modern Theme - Sidebar Override
Modern left navigation sidebar with collapsible state and icons
#}
<nav class="ots-sidebar" aria-label="Main navigation">
<div class="sidebar-header">
<a href="{{ baseUrl }}/" class="brand-link">
<span class="brand-icon">🎯</span>
<span class="brand-text">OTS Signs</span>
</a>
<button class="sidebar-close-btn" aria-label="Close sidebar">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div id="sidebar-wrapper" class="ots-sidebar-wrapper">
<ul class="sidebar ots-sidebar">
<li class="sidebar-main">
<a href="{{ url_for("home") }}">
<span class="ots-nav-icon fa fa-home" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Dashboard" %}</span>
</a>
</li>
<div class="sidebar-content">
<ul class="sidebar-nav">
<li>
<a href="{{ baseUrl }}" class="nav-item {% if pageTitle == 'Dashboard' %}active{% endif %}">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</svg>
<span class="nav-text">Dashboard</span>
</a>
</li>
{% if currentUser.featureEnabled("schedule.view") %}
<li class="sidebar-list">
<a href="{{ url_for("schedule.view") }}">
<span class="ots-nav-icon fa fa-calendar" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Schedule" %}</span>
</a>
</li>
{% endif %}
<li class="nav-section-divider">
<span class="nav-label">Content</span>
</li>
<li><a href="{{ baseUrl }}/library" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-text">Media Library</span>
</a></li>
<li><a href="{{ baseUrl }}/layout" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>
</svg>
<span class="nav-text">Layouts</span>
</a></li>
<li><a href="{{ baseUrl }}/playlist" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
</svg>
<span class="nav-text">Playlists</span>
</a></li>
{% if currentUser.featureEnabled("daypart.view") %}
<li class="sidebar-list">
<a href="{{ url_for("daypart.view") }}">
<span class="ots-nav-icon fa fa-clock" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Dayparting" %}</span>
</a>
</li>
{% endif %}
<li class="nav-section-divider">
<span class="nav-label">Displays</span>
</li>
<li><a href="{{ baseUrl }}/display" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M12 17v4"/><path d="M8 21h8"/>
</svg>
<span class="nav-text">Displays</span>
</a></li>
<li><a href="{{ baseUrl }}/display-group" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="8" height="8" rx="1" ry="1"/><rect x="14" y="2" width="8" height="8" rx="1" ry="1"/><rect x="2" y="14" width="8" height="8" rx="1" ry="1"/><rect x="14" y="14" width="8" height="8" rx="1" ry="1"/>
</svg>
<span class="nav-text">Display Groups</span>
</a></li>
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Design" %}</a></li>
{% if currentUser.featureEnabled("campaign.view") %}
<li class="sidebar-list">
<a href="{{ url_for("campaign.view") }}">
<span class="ots-nav-icon fa fa-bullhorn" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Campaigns" %}</span>
</a>
</li>
{% endif %}
<li class="nav-section-divider">
<span class="nav-label">Scheduling</span>
</li>
<li><a href="{{ baseUrl }}/schedule" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><path d="M16 2v4"/><path d="M8 2v4"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<span class="nav-text">Schedules</span>
</a></li>
<li><a href="{{ baseUrl }}/dayparting" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9"/><polyline points="12 6 12 12 16 14"/>
</svg>
<span class="nav-text">Day Parting</span>
</a></li>
{% if currentUser.featureEnabled("layout.view") %}
<li class="sidebar-list">
<a href="{{ url_for("layout.view") }}">
<span class="ots-nav-icon fa fa-columns" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Layouts" %}</span>
</a>
</li>
{% endif %}
<li class="nav-section-divider">
<span class="nav-label">Administration</span>
</li>
<li><a href="{{ baseUrl }}/user" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
<span class="nav-text">Users</span>
</a></li>
<li><a href="{{ baseUrl }}/user-group" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-text">User Groups</span>
</a></li>
<li><a href="{{ baseUrl }}/settings" class="nav-item">
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6M4.22 4.22l4.24 4.24m2.12 2.12l4.24 4.24M1 12h6m6 0h6m-16.78 7.78l4.24-4.24m2.12-2.12l4.24-4.24"/>
</svg>
<span class="nav-text">Settings</span>
</a></li>
{% if currentUser.featureEnabled("template.view") %}
<li class="sidebar-list">
<a href="{{ url_for("template.view") }}">
<span class="ots-nav-icon fa fa-clone" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Templates" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("resolution.view") %}
<li class="sidebar-list">
<a href="{{ url_for("resolution.view") }}">
<span class="ots-nav-icon fa fa-expand" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Resolutions" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Library" %}</a></li>
{% if currentUser.featureEnabled("playlist.view") %}
<li class="sidebar-list">
<a href="{{ url_for("playlist.view") }}">
<span class="ots-nav-icon fa fa-list" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Playlists" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("library.view") %}
<li class="sidebar-list">
<a href="{{ url_for("library.view") }}">
<span class="ots-nav-icon fa fa-photo" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Media" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<li class="sidebar-list">
<a href="{{ url_for("dataset.view") }}">
<span class="ots-nav-icon fa fa-database" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "DataSets" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("menuBoard.view") %}
<li class="sidebar-list">
<a href="{{ url_for("menuBoard.view") }}">
<span class="ots-nav-icon fa fa-th-large" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Menu Boards" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Displays" %}</a></li>
{% if currentUser.featureEnabled("displays.view") %}
<li class="sidebar-list">
<a href="{{ url_for("display.view") }}">
<span class="ots-nav-icon fa fa-desktop" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Displays" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<li class="sidebar-list">
<a href="{{ url_for("displaygroup.view") }}">
<span class="ots-nav-icon fa fa-object-group" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Display Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("display.syncView") %}
<li class="sidebar-list">
<a href="{{ url_for("syncgroup.view") }}">
<span class="ots-nav-icon fa fa-link" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Sync Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
<li class="sidebar-list">
<a href="{{ url_for("displayprofile.view") }}">
<span class="ots-nav-icon fa fa-sliders" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Display Settings" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("playersoftware.view") %}
<li class="sidebar-list">
<a href="{{ url_for("playersoftware.view") }}">
<span class="ots-nav-icon fa fa-download" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Player Versions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("command.view") %}
<li class="sidebar-list">
<a href="{{ url_for("command.view") }}">
<span class="ots-nav-icon fa fa-terminal" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Commands" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
{% set userMenuViewable = true %}
{% else %}
{% set userMenuViewable = false %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view"]) %}
{% if countViewable > 0 or userMenuViewable %}
<li class="sidebar-title"><a>{% trans "Administration" %}</a></li>
{% if userMenuViewable %}
<li class="sidebar-list">
<a href="{{ url_for("user.view") }}">
<span class="ots-nav-icon fa fa-users" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Users" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("usergroup.view") %}
<li class="sidebar-list">
<a href="{{ url_for("group.view") }}">
<span class="ots-nav-icon fa fa-users-cog" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "User Groups" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("admin.view") }}">
<span class="ots-nav-icon fa fa-wrench" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Settings" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("application.view") }}">
<span class="ots-nav-icon fa fa-th" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Applications" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("module.view") %}
<li class="sidebar-list">
<a href="{{ url_for("module.view") }}">
<span class="ots-nav-icon fa fa-puzzle-piece" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Modules" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("transition.view") %}
<li class="sidebar-list">
<a href="{{ url_for("transition.view") }}">
<span class="ots-nav-icon fa fa-exchange" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Transitions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("task.view") %}
<li class="sidebar-list">
<a href="{{ url_for("task.view") }}">
<span class="ots-nav-icon fa fa-tasks" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Tasks" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("tag.view") %}
<li class="sidebar-list">
<a href="{{ url_for("tag.view") }}">
<span class="ots-nav-icon fa fa-tags" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Tags" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.isSuperAdmin() %}
<li class="sidebar-list">
<a href="{{ url_for("folders.view") }}">
<span class="ots-nav-icon fa fa-folder" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Folders" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("font.view") %}
<li class="sidebar-list">
<a href="{{ url_for("font.view") }}">
<span class="ots-nav-icon fa fa-font" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Fonts" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["report.view", "report.scheduling", "report.saving"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Reporting" %}</a></li>
{% if currentUser.featureEnabled("report.view") %}
<li class="sidebar-list">
<a href="{{ url_for("report.view") }}">
<span class="ots-nav-icon fa fa-file-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "All Reports" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.scheduling") %}
<li class="sidebar-list">
<a href="{{ url_for("reportschedule.view") }}">
<span class="ots-nav-icon fa fa-calendar-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Report Schedules" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("report.saving") %}
<li class="sidebar-list">
<a href="{{ url_for("savedreport.view") }}">
<span class="ots-nav-icon fa fa-save" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Saved Reports" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["log.view", "sessions.view", "auditlog.view", "fault.view"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Advanced" %}</a></li>
{% if currentUser.featureEnabled("log.view") %}
<li class="sidebar-list">
<a href="{{ url_for("log.view") }}">
<span class="ots-nav-icon fa fa-list-alt" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Log" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("sessions.view") %}
<li class="sidebar-list">
<a href="{{ url_for("sessions.view") }}">
<span class="ots-nav-icon fa fa-history" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Sessions" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("auditlog.view") %}
<li class="sidebar-list">
<a href="{{ url_for("auditlog.view") }}">
<span class="ots-nav-icon fa fa-clipboard-list" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Audit Trail" %}</span>
</a>
</li>
{% endif %}
{% if currentUser.featureEnabled("fault.view") %}
<li class="sidebar-list">
<a href="{{ url_for("fault.view") }}">
<span class="ots-nav-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Report Fault" %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["developer.edit"]) %}
{% if countViewable > 0 %}
<li class="sidebar-title"><a>{% trans "Developer" %}</a></li>
{% if currentUser.featureEnabled("developer.edit") %}
<li class="sidebar-list">
<a href="{{ url_for("developer.templates.view") }}">
<span class="ots-nav-icon fa fa-code" aria-hidden="true"></span>
<span class="ots-nav-text">{% trans "Module Templates" %}</span>
</a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar user-avatar-lg">{{ user.username|first|upper }}</div>
<div class="user-details">
<div class="user-name">{{ user.username }}</div>
<div class="user-role text-xs">Administrator</div>
</div>
</div>
</div>
</nav>
</div>

View File

@@ -0,0 +1,8 @@
{#
OTS Signage Theme override
Optional include rendered in authed.twig (top right navbar)
Minimal, low-risk addition for verification
#}
<li class="nav-item ots-theme-badge">
<span class="nav-link">OTS Theme</span>
</li>

View File

@@ -0,0 +1,472 @@
{#
/**
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
<ul class="nav navbar-nav ots-topbar">
<li class="nav-item">
<a class="nav-link" href="{{ url_for("home") }}">
<span class="ots-topbar-icon fa fa-home" aria-hidden="true"></span>
{% trans "Dashboard" %}
</a>
</li>
{% set countViewable = currentUser.featureEnabledCount(["schedule.view", "daypart.view"]) %}
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 %}
{% if countViewable > 1 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-calendar" aria-hidden="true"></span>
{% trans "Schedule" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% else %}
<li class="nav-item">
{% endif %}
{% if currentUser.featureEnabled("schedule.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("schedule.view") }}">
<span class="ots-topbar-icon fa fa-calendar" aria-hidden="true"></span>
{% trans "Schedule" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("daypart.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("daypart.view") }}">
<span class="ots-topbar-icon fa fa-clock" aria-hidden="true"></span>
{% trans "Dayparting" %}
</a>
{% endif %}
{% if countViewable > 1 %}
</div>
{% endif %}
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["campaign.view", "layout.view", "template.view", "resolution.view"]) %}
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 %}
{% if countViewable > 1 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-paint-brush" aria-hidden="true"></span>
{% trans "Design" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% else %}
<li class="nav-item">
{% endif %}
{% if currentUser.featureEnabled("campaign.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("campaign.view") }}">
<span class="ots-topbar-icon fa fa-bullhorn" aria-hidden="true"></span>
{% trans "Campaigns" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("layout.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("layout.view") }}">
<span class="ots-topbar-icon fa fa-columns" aria-hidden="true"></span>
{% trans "Layouts" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("template.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("template.view") }}">
<span class="ots-topbar-icon fa fa-clone" aria-hidden="true"></span>
{% trans "Templates" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("resolution.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("resolution.view") }}">
<span class="ots-topbar-icon fa fa-expand" aria-hidden="true"></span>
{% trans "Resolutions" %}
</a>
{% endif %}
{% if countViewable > 1 %}
</div>
{% endif %}
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["library.view", "playlist.view", "dataset.view", "menuBoard.view"]) %}
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 %}
{% if countViewable > 1 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-folder-open" aria-hidden="true"></span>
{% trans "Library" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% else %}
<li class="nav-item">
{% endif %}
{% if currentUser.featureEnabled("playlist.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("playlist.view") }}">
<span class="ots-topbar-icon fa fa-list" aria-hidden="true"></span>
{% trans "Playlists" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("library.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("library.view") }}">
<span class="ots-topbar-icon fa fa-photo" aria-hidden="true"></span>
{% trans "Media" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("dataset.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("dataset.view") }}">
<span class="ots-topbar-icon fa fa-database" aria-hidden="true"></span>
{% trans "DataSets" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("menuBoard.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("menuBoard.view") }}">
<span class="ots-topbar-icon fa fa-th-large" aria-hidden="true"></span>
{% trans "Menu Boards" %}
</a>
{% endif %}
{% if countViewable > 1 %}
</div>
{% endif %}
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["displays.view", "displaygroup.view", "displayprofile.view", "playersoftware.view", "command.view"]) %}
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 %}
{% if countViewable > 1 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-desktop" aria-hidden="true"></span>
{% trans "Displays" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% else %}
<li class="nav-item">
{% endif %}
{% if currentUser.featureEnabled("displays.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("display.view") }}">
<span class="ots-topbar-icon fa fa-desktop" aria-hidden="true"></span>
{% trans "Displays" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("displaygroup.view") }}">
<span class="ots-topbar-icon fa fa-object-group" aria-hidden="true"></span>
{% trans "Display Groups" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("display.syncView") %}
<a class="{{ groupElementClass }}" href="{{ url_for("syncgroup.view") }}">
<span class="ots-topbar-icon fa fa-link" aria-hidden="true"></span>
{% trans "Sync Groups" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("displayprofile.view") }}">
<span class="ots-topbar-icon fa fa-sliders" aria-hidden="true"></span>
{% trans "Display Settings" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("playersoftware.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("playersoftware.view") }}">
<span class="ots-topbar-icon fa fa-download" aria-hidden="true"></span>
{% trans "Player Versions" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("command.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("command.view") }}">
<span class="ots-topbar-icon fa fa-terminal" aria-hidden="true"></span>
{% trans "Commands" %}
</a>
{% endif %}
{% if countViewable > 1 %}
</div>
{% endif %}
</li>
{% endif %}
{% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
{% set userMenuViewable = true %}
{% else %}
{% set userMenuViewable = false %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["usergroup.view", "module.view", "transition.view", "task.view"]) %}
{% set groupElementClass = (countViewable > 1 or (countViewable == 1 and userMenuViewable)) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 or userMenuViewable %}
{% if countViewable > 1 or (countViewable == 1 and userMenuViewable) %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-cog" aria-hidden="true"></span>
{% trans "Administration" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% endif %}
{% if userMenuViewable %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("user.view") }}">
<span class="ots-topbar-icon fa fa-users" aria-hidden="true"></span>
{% trans "Users" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("usergroup.view") %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("group.view") }}">
<span class="ots-topbar-icon fa fa-users-cog" aria-hidden="true"></span>
{% trans "User Groups" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.isSuperAdmin() %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("admin.view") }}">
<span class="ots-topbar-icon fa fa-wrench" aria-hidden="true"></span>
{% trans "Settings" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.isSuperAdmin() %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("application.view") }}">
<span class="ots-topbar-icon fa fa-th" aria-hidden="true"></span>
{% trans "Applications" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("module.view") %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("module.view") }}">
<span class="ots-topbar-icon fa fa-puzzle-piece" aria-hidden="true"></span>
{% trans "Modules" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("transition.view") %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("transition.view") }}">
<span class="ots-topbar-icon fa fa-exchange" aria-hidden="true"></span>
{% trans "Transitions" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("task.view") %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("task.view") }}">
<span class="ots-topbar-icon fa fa-tasks" aria-hidden="true"></span>
{% trans "Tasks" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("tag.view") %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("tag.view") }}">
<span class="ots-topbar-icon fa fa-tags" aria-hidden="true"></span>
{% trans "Tags" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.isSuperAdmin() %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("folders.view") }}">
<span class="ots-topbar-icon fa fa-folder" aria-hidden="true"></span>
{% trans "Folders" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if currentUser.featureEnabled("font.view") %}
{% if countViewable == 0 %}
<li class="nav-item">
{% endif %}
<a class="{{ groupElementClass }}" href="{{ url_for("font.view") }}">
<span class="ots-topbar-icon fa fa-font" aria-hidden="true"></span>
{% trans "Fonts" %}
</a>
{% if countViewable == 0 %}
</li>
{% endif %}
{% endif %}
{% if countViewable > 1 or (countViewable == 1 and userMenuViewable) %}
</div>
{% endif %}
{% if countViewable > 1 or (countViewable == 1 and userMenuViewable) %}
</li>
{% endif %}
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["report.view", "report.scheduling", "report.saving"]) %}
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 %}
{% if countViewable > 1 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-chart-bar" aria-hidden="true"></span>
{% trans "Reporting" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% else %}
<li class="nav-item">
{% endif %}
{% if currentUser.featureEnabled("report.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("report.view") }}">
<span class="ots-topbar-icon fa fa-file-alt" aria-hidden="true"></span>
{% trans "All Reports" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("report.scheduling") %}
<a class="{{ groupElementClass }}" href="{{ url_for("reportschedule.view") }}">
<span class="ots-topbar-icon fa fa-calendar-alt" aria-hidden="true"></span>
{% trans "Report Schedules" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("report.saving") %}
<a class="{{ groupElementClass }}" href="{{ url_for("savedreport.view") }}">
<span class="ots-topbar-icon fa fa-save" aria-hidden="true"></span>
{% trans "Saved Reports" %}
</a>
{% endif %}
{% if countViewable > 1 %}
</div>
{% endif %}
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["log.view", "sessions.view", "auditlog.view", "fault.view"]) %}
{% set groupElementClass = (countViewable > 1) ? 'dropdown-item' : 'nav-link' %}
{% if countViewable > 0 %}
{% if countViewable > 1 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-shield-alt" aria-hidden="true"></span>
{% trans "Advanced" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% else %}
<li class="nav-item">
{% endif %}
{% if currentUser.featureEnabled("log.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("log.view") }}">
<span class="ots-topbar-icon fa fa-list-alt" aria-hidden="true"></span>
{% trans "Log" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("sessions.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("sessions.view") }}">
<span class="ots-topbar-icon fa fa-history" aria-hidden="true"></span>
{% trans "Sessions" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("auditlog.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("auditlog.view") }}">
<span class="ots-topbar-icon fa fa-clipboard-list" aria-hidden="true"></span>
{% trans "Audit Trail" %}
</a>
{% endif %}
{% if currentUser.featureEnabled("fault.view") %}
<a class="{{ groupElementClass }}" href="{{ url_for("fault.view") }}">
<span class="ots-topbar-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
{% trans "Report Fault" %}
</a>
{% endif %}
{% if countViewable > 1 %}
</div>
{% endif %}
</li>
{% endif %}
{% set countViewable = currentUser.featureEnabledCount(["developer.edit"]) %}
{% if countViewable > 0 %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="ots-topbar-icon fa fa-code" aria-hidden="true"></span>
{% trans "Developer" %} <span class="caret"></span>
</a>
<div class="dropdown-menu">
{% if currentUser.featureEnabled("developer.edit") %}
<a class="dropdown-item" href="{{ url_for("developer.templates.view") }}">
<span class="ots-topbar-icon fa fa-code-branch" aria-hidden="true"></span>
{% trans "Module Templates" %}
</a>
{% endif %}
</div>
</li>
{% endif %}
</ul>

View File

@@ -0,0 +1,33 @@
{#
OTS Signage Theme override
Based on Xibo CMS default authed-user-menu.twig (master branch)
Minimal change: add ots-user-menu class for easy verification
#}
<li class="dropdown nav-item item">
<a href="#" class="nav-link" data-toggle="dropdown" id="navbarUserMenu">
<img class="nav-avatar" src="{{ theme.uri("img/avatar.jpg") }}" />
</a>
<div class="dropdown-menu dropdown-menu-right ots-user-menu" aria-labelledby="navbarUserMenu">
<h6 class="dropdown-header">{{ currentUser.userName }}<br/>
<div id="XiboClock">{{ clock }}</div>
</h6>
<div class="dropdown-divider"></div>
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.preferences.form") }}" title="{% trans "Preferences" %}">{% trans "Preferences" %}</a>
{% if currentUser.featureEnabled("user.profile") %}
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.edit.profile.form") }}" title="{% trans "Edit Profile" %}">{% trans "Edit Profile" %}</a>
{% endif %}
<a class="dropdown-item XiboFormButton" href="{{ url_for("user.applications") }}" title="{% trans "View my authenticated applications" %}">{% trans "My Applications" %}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" id="reshowWelcomeMenuItem" href="{{ url_for("welcome.view") }}">{% trans "Reshow welcome" %}</a>
<a class="dropdown-item XiboFormButton" href="{{ url_for("about") }}" title="{% trans "About the CMS" %}">{% trans "About" %}</a>
{% if not hideLogout %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" title="{% trans "Logout" %}" href="{{ logoutUrl }}">{% trans "Logout" %}</a>
{% endif %}
</div>
</li>

View File

@@ -1,83 +0,0 @@
{#
OTS Signage Modern Theme - Authenticated Shell Override
Replaces the header and shell structure with a modern topbar + sidebar layout
#}
{% extends "base.twig" %}
{% block head %}
{{ parent() }}
<link rel="stylesheet" href="{{ baseUrl }}/theme/custom/otssignange/css/override.css" />
<link rel="stylesheet" href="{{ baseUrl }}/theme/custom/otssignange/css/client.css" />
{% endblock %}
{% block htmlTag %}
<html lang="en" data-ots-theme="v1" data-mode="dark">
{% endblock %}
{% block body %}
<body class="ots-theme ots-dark-mode">
<div class="ots-shell">
{% include "authed-sidebar.twig" %}
<div class="ots-main">
{% block header %}
<header class="ots-topbar">
<div class="topbar-left">
<button class="btn-ghost topbar-toggle" data-action="toggle-sidebar" aria-label="Toggle sidebar" title="Toggle sidebar">
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<div class="topbar-title">
<h1 class="page-title">{{ pageTitle|default('Dashboard') }}</h1>
{% if pageSubtitle is defined %}<p class="page-subtitle">{{ pageSubtitle }}</p>{% endif %}
</div>
</div>
<div class="topbar-right">
<form action="{{ baseUrl }}/search" class="topbar-search" method="get" role="search">
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input type="text" name="q" placeholder="Search…" aria-label="Search" class="search-input" />
</form>
<div class="topbar-actions">
<button class="topbar-btn" aria-label="Notifications" title="Notifications">
<svg class="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>
</button>
<div class="dropdown user-menu">
<button class="topbar-btn user-btn" aria-label="User menu" aria-expanded="false">
<span class="avatar avatar-sm">{{ user.username|first|upper }}</span>
</button>
<ul class="dropdown-menu dropdown-right" role="menu">
<li><a href="{{ baseUrl }}/profile" role="menuitem">Profile</a></li>
<li><a href="{{ baseUrl }}/logout" role="menuitem">Sign out</a></li>
</ul>
</div>
</div>
</div>
</header>
{% endblock %}
<main class="ots-content">
{% block content %}
<!-- Content inserted here by page templates -->
{% endblock %}
</main>
{% block footer %}
<footer class="ots-footer">
<p class="text-muted text-xs">&copy; {{ currentDate|date('Y') }} {{ app_name }}. Powered by <a href="https://xibosignage.com">Xibo</a>.</p>
</footer>
{% endblock %}
</div>
</div>
{% block scripts %}
{{ parent() }}
<script src="{{ baseUrl }}/theme/custom/otssignange/js/theme.js"></script>
{% endblock %}
</body>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,131 +0,0 @@
{#
OTS Signage Modern Theme - Dashboard Page Override
Modern dashboard with KPI cards, status panels, and quick actions
#}
{% extends "authed.twig" %}
{% block pageTitle %}Dashboard{% endblock %}
{% block content %}
<div class="ots-theme dashboard-page">
{# KPI Cards Row #}
<section class="kpi-section">
<div class="kpi-card">
<div class="kpi-header">
<h3 class="kpi-label">Displays</h3>
<span class="kpi-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M12 17v4"/><path d="M8 21h8"/>
</svg>
</span>
</div>
<div class="kpi-body">
<div class="kpi-number">1</div>
<div class="kpi-meta">100% Displays Online</div>
</div>
<div class="kpi-footer">
<span class="badge badge-success">1</span>
<span class="text-xs text-muted">Online</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-header">
<h3 class="kpi-label">Schedules</h3>
<span class="kpi-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><path d="M16 2v4"/><path d="M8 2v4"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</span>
</div>
<div class="kpi-body">
<div class="kpi-number">0</div>
<div class="kpi-meta">Scheduled events</div>
</div>
<div class="kpi-footer">
<span class="badge badge-secondary">0</span>
<span class="text-xs text-muted">Upcoming</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-header">
<h3 class="kpi-label">Users</h3>
<span class="kpi-icon-box">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
</span>
</div>
<div class="kpi-body">
<div class="kpi-number">2</div>
<div class="kpi-meta">OTS Signs users</div>
</div>
<div class="kpi-footer">
<span class="badge badge-info">2</span>
<span class="text-xs text-muted">Active</span>
</div>
</div>
</section>
{# Main Panels Row #}
<section class="dashboard-panels">
<article class="panel panel-full">
<div class="panel-header">
<h3>Display Status</h3>
<a href="{{ baseUrl }}/display" class="link-secondary">View all →</a>
</div>
<div class="panel-body">
<div class="empty-state-compact">
<p class="text-muted">You have 1 display configured. Last check-in: just now</p>
<a href="{{ baseUrl }}/display" class="btn btn-outline btn-sm">Manage Displays</a>
</div>
</div>
</article>
<article class="panel panel-full">
<div class="panel-header">
<h3>Upcoming Schedules</h3>
<a href="{{ baseUrl }}/schedule" class="link-secondary">View all →</a>
</div>
<div class="panel-body">
<div class="empty-state-compact">
<p class="text-muted">No schedules found. Create a schedule to get started.</p>
<a href="{{ baseUrl }}/schedule/add" class="btn btn-outline btn-sm">Create Schedule</a>
</div>
</div>
</article>
</section>
{# Quick Actions Section #}
<section class="quick-actions-grid">
<h3 class="section-title">Quick Actions</h3>
<div class="action-cards">
<a href="{{ baseUrl }}/schedule/add" class="action-card">
<div class="action-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><path d="M16 2v4"/><path d="M8 2v4"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<span class="action-label">Create Schedule</span>
</a>
<a href="{{ baseUrl }}/display" class="action-card">
<div class="action-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M12 17v4"/><path d="M8 21h8"/>
</svg>
</div>
<span class="action-label">Manage Displays</span>
</a>
<a href="{{ baseUrl }}/user/add" class="action-card">
<div class="action-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
</div>
<span class="action-label">Add User</span>
</a>
</div>
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,66 @@
/* High-specificity DataTables contrast overrides
Ensures table body text is readable against dark theme backgrounds.
Light text on dark backgrounds.
*/
#datatable-container table.dataTable tbody td,
#datatable-container .dataTables_wrapper table.dataTable tbody td,
.ots-table-card table.dataTable tbody td,
.ots-table-card table.dataTable tbody td * {
color: #f5f5f5 !important;
opacity: 1 !important;
}
#datatable-container table.dataTable thead th,
.ots-table-card table.dataTable thead th,
#datatable-container table.dataTable thead th * {
color: #ffffff !important;
opacity: 1 !important;
background-color: rgba(0,0,0,0.3) !important;
}
#datatable-container table.dataTable tbody tr.table-success td,
#datatable-container table.dataTable tbody tr.success td,
#datatable-container table.dataTable tbody tr.selected td,
#datatable-container table.dataTable tbody tr.highlight td {
background-color: rgba(255,255,255,0.08) !important;
color: #ffffff !important;
}
#datatable-container table.dataTable tbody td .btn,
#datatable-container table.dataTable tbody td .badge,
#datatable-container table.dataTable tbody td .dropdown-toggle {
color: #ffffff !important;
}
#datatable-container table.dataTable tbody tr {
background-color: rgba(0,0,0,0.1) !important;
}
#datatable-container table.dataTable tbody tr:hover {
background-color: rgba(255,255,255,0.05) !important;
}
.dataTables_wrapper .dataTables_filter input,
.dataTables_wrapper .dataTables_length select,
.dataTables_wrapper .dataTables_paginate .paginate_button {
color: #f5f5f5 !important;
background: rgba(0,0,0,0.2) !important;
border-color: rgba(255,255,255,0.1) !important;
}
#datatable-container table.dataTable tbody td img,
#datatable-container table.dataTable tbody td svg {
filter: none !important;
}
#datatable-container table.dataTable thead th.sorting:after,
#datatable-container table.dataTable thead th.sorting_asc:after,
#datatable-container table.dataTable thead th.sorting_desc:after {
color: rgba(255,255,255,0.7) !important;
}
.ots-table-card table.dataTable tbody tr td,
.ots-table-card table.dataTable tbody tr td * {
-webkit-text-fill-color: #f5f5f5 !important;
}

View File

@@ -0,0 +1,384 @@
{#
/**
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Displays"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("displays.add") %}
<button class="btn btn-success XiboFormButton" title="{% trans "Add a Display via user_code displayed on the Player screen" %}" href="{{ url_for("display.addViaCode.form") }}"> <i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Display (Code)" %}</button>
{% endif %}
<button class="btn btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block headContent %}
{# Add page source code bundle ( CSS ) #}
<link rel="stylesheet" href="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.css?v={{ version }}&rev={{revision }}">
{% endblock %}
{% block pageContent %}
<div class="ots-displays-page">
<div class="page-header ots-page-header">
<h1>{% trans "Displays" %}</h1>
<p class="text-muted">{% trans "Manage your player fleet and status." %}</p>
</div>
<div class="widget dashboard-card ots-displays-card">
<div class="widget-title ots-displays-title">{% trans "Displays" %}</div>
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayView">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
<h3 class="ots-filter-title">{% trans "Filter Displays" %}</h3>
<button type="button" class="ots-filter-toggle" id="ots-filter-collapse-btn" title="{% trans 'Toggle filter panel' %}">
<i class="fa fa-chevron-up"></i>
</button>
</div>
<div class="ots-filter-content" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a class="nav-link active" href="#filter-general" role="tab" data-toggle="tab">{% trans "General" %}</a></li>
<li class="nav-item"><a class="nav-link" href="#filter-advanced" role="tab" data-toggle="tab">{% trans "Advanced" %}</a></li>
</ul>
<form class="form-inline">
<div class="tab-content">
<div class="tab-pane active" id="filter-general">
{% set title %}{% trans "ID" %}{% endset %}
{{ inline.number("displayId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('display', title) }}
{% set title %}{% trans "Status" %}{% endset %}
{% set check %}{% trans "Up to date" %}{% endset %}
{% set cross %}{% trans "Downloading" %}{% endset %}
{% set cloud %}{% trans "Out of date" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: check},
{ optionid: "2", option: cross},
{ optionid: "3", option: cloud}
] %}
{{ inline.dropdown("mediaInventoryStatus", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Logged In?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: yesOption},
{ optionid: "0", option: noOption}
] %}
{{ inline.dropdown("loggedIn", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Authorised?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: yesOption },
{ optionid: "0", option: noOption},
] %}
{{ inline.dropdown("authorised", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "XMR Registered?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: 1, option: yesOption},
{ optionid: 0, option: noOption},
] %}
{{ inline.dropdown("xmrRegistered", "single", title, "", options, "optionid", "option") }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-filter-options", value: '{"isDisplaySpecific":0}' },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" },
{ name: "data-initial-key", value: "displayGroupId" },
] %}
{{ inline.dropdown("displayGroupId", "single", title, "", null, "displayGroupId", "displayGroup", helpText, "pagedSelect", "", "", "", attributes) }}
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
{% set title %}{% trans "Display Profile" %}{% endset %}
{{ inline.dropdown("displayProfileId", "single", title, "", [{displayProfileId:null, name:""}]|merge(displayProfiles), "displayProfileId", "name") }}
{% endif %}
{{ inline.hidden("folderId") }}
</div>
<div class="tab-pane" id="filter-advanced">
{% set title %}{% trans "Last Accessed" %}{% endset %}
{{ inline.date("lastAccessed", title) }}
{% set title %}{% trans "Player Type" %}{% endset %}
{% set android %}{% trans "Android" %}{% endset %}
{% set chromeos %}{% trans "ChromeOS" %}{% endset %}
{% set windows %}{% trans "Windows" %}{% endset %}
{% set webos %}{% trans "webOS" %}{% endset %}
{% set sssp %}{% trans "Tizen" %}{% endset %}
{% set linux %}{% trans "Linux" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "android", option: android},
{ optionid: "chromeos", option: chromeos},
{ optionid: "windows", option: windows},
{ optionid: "lg", option: webos},
{ optionid: "sssp", option: sssp},
{ optionid: "linux", option: linux},
] %}
{{ inline.dropdown("clientType", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Player Code" %}{% endset %}
{{ inline.input("clientCode", title) }}
{% set title %}{% trans "Custom ID" %}{% endset %}
{{ inline.input("customId", title) }}
{% set title %}{% trans "Mac Address" %}{% endset %}
{{ inline.input("macAddress", title) }}
{% set title %}{% trans "IP Address" %}{% endset %}
{{ inline.input("clientAddress", title) }}
{% set title %}{% trans "Orientation" %}{% endset %}
{% set landscape %}{% trans "Landscape" %}{% endset %}
{% set portrait %}{% trans "Portrait" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "landscape", option: landscape},
{ optionid: "portrait", option: portrait}
] %}
{{ inline.dropdown("orientation", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Commercial Licence" %}{% endset %}
{% set licensed %}{% trans "Licensed fully" %}{% endset %}
{% set trial %}{% trans "Trial" %}{% endset %}
{% set notLinceced %}{% trans "Not licenced" %}{% endset %}
{% set notApplicable %}{% trans "Not applicable" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: licensed},
{ optionid: "2", option: trial},
{ optionid: "0", option: notLinceced},
{ optionid: "3", option: notApplicable}
] %}
{{ inline.dropdown("commercialLicence", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Player supported?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: 1, option: yesOption},
{ optionid: 0, option: noOption},
] %}
{{ inline.dropdown("isPlayerSupported", "single", title, "", options, "optionid", "option") }}
</div>
</div>
</form>
</div>
</div>
</div>
<div class="grid-with-folders-container ots-grid-with-folders">
<div class="grid-folder-tree-container p-3 dashboard-card ots-folder-tree" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none ots-grid-controller">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{{ "Open / Close Folder Search options"|trans }}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div class="map-controller d-none pl-1 ots-grid-controller">
<button type="button" id="map_button" class="btn btn-primary" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
</div>
<div class="list-controller d-none pl-1 ots-grid-controller">
<button type="button" id="list_button" class="btn btn-primary" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
</div>
<div id="datatable-container">
<div class="XiboData card py-3 dashboard-card ots-table-card">
<table id="displays" class="table table-striped" data-content-type="display" data-content-id-name="displayId" data-state-preference-name="displayGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Display Type" %}</th>
<th>{% trans "Address" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Authorised?" %}</th>
<th>{% trans "Current Layout" %}</th>
<th>{% trans "Storage Available" %}</th>
<th>{% trans "Storage Total" %}</th>
<th>{% trans "Storage Free %" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Orientation" %}</th>
<th>{% trans "Resolution" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
<th>{% trans "Default Layout" %}</th>
<th>{% trans "Interleave Default" %}</th>
<th>{% trans "Email Alert" %}</th>
<th>{% trans "Logged In" %}</th>
<th>{% trans "Last Accessed" %}</th>
<th>{% trans "Display Profile" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Supported?" %}</th>
<th>{% trans "Device Name" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Mac Address" %}</th>
<th>{% trans "Timezone" %}</th>
<th>{% trans "Languages" %}</th>
<th>{% trans "Latitude" %}</th>
<th>{% trans "Longitude" %}</th>
<th>{% trans "Screen shot?" %}</th>
<th>{% trans "Thumbnail" %}</th>
<th>{% trans "CMS Transfer?" %}</th>
<th>{% trans "Bandwidth Limit" %}</th>
<th>{% trans "Last Command" %}</th>
<th>{% trans "XMR Registered" %}</th>
<th>{% trans "Commercial Licence" %}</th>
<th>{% trans "Remote" %}</th>
<th>{% trans "Sharing" %}</th>
<th>{% trans "Screen Size" %}</th>
<th>{% trans "Is Mobile?" %}</th>
<th>{% trans "Outdoor?" %}</th>
<th>{% trans "Reference 1" %}</th>
<th>{% trans "Reference 2" %}</th>
<th>{% trans "Reference 3" %}</th>
<th>{% trans "Reference 4" %}</th>
<th>{% trans "Reference 5" %}</th>
<th>{% trans "Custom ID" %}</th>
<th>{% trans "Cost Per Play" %}</th>
<th>{% trans "Impressions Per Play" %}</th>
<th>{% trans "Created Date" %}</th>
<th>{% trans "Modified Date" %}</th>
<th>{% trans "Faults?" %}</th>
<th>{% trans "OS Version" %}</th>
<th>{% trans "OS SDK" %}</th>
<th>{% trans "Manufacturer" %}</th>
<th>{% trans "Brand" %}</th>
<th>{% trans "Model" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!-- Map -->
<div class="row">
<div class="col-sm-12">
<div class="map-legend" style="display:none; position: absolute; z-index: 500; right: 20px; top: 10px;">
<div class="display-map-legend" style="font-size: 12px;">
<div>Logged in</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-green-check.png'/> - Up to date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-yellow-check.png'/> - Out of date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-red-check.png'/> - Downloading/Unknown</div>
</br>
<div>Logged out</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-green-cross.png'/> - Up to date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-yellow-cross.png'/> - Out of date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-red-cross.png'/> - Downloading/Unknown</div>
</div>
</div>
<div id="display-map" class="dashboard-card ots-map-card" data-displays-url="{{ url_for("display.map") }}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
{# Initialise JS variables and translations #}
<script type="text/javascript" nonce="{{ cspNonce }}" defer>
{# JS variables #}
var publicPath = "{{ theme.rootUri() }}";
var displaySearchURL = "{{ url_for('display.search') }}";
var layoutSearchURL = "{{ url_for('layout.search') }}";
var mapConfig = {{ mapConfig| json_encode | raw }};
var playerVersionSupport = "{{playerVersion}}";
var folderViewEnabled = "{{ currentUser.featureEnabled('folder.view') }}";
var taggingEnabled = "{{ currentUser.featureEnabled('tag.tagging') }}";
var showThumbnailColumn = "{{ currentUser.getOptionValue('showThumbnailColumn', 1) }}";
var SHOW_DISPLAY_AS_VNCLINK = "{{ settings.SHOW_DISPLAY_AS_VNCLINK }}";
var SHOW_DISPLAY_AS_VNC_TGT = "{{ settings.SHOW_DISPLAY_AS_VNC_TGT }}";
{# Custom translations #}
var displayPageTrans = {
back: "{% trans "Back" %}",
yes: "{% trans "Yes" %}",
no: "{% trans "No" %}",
daysOfTheWeek: {
monday: "{% trans "Monday" %}",
tuesday: "{% trans "Tuesday" %}",
wednesday: "{% trans "Wednesday" %}",
thursday: "{% trans "Thursday" %}",
friday: "{% trans "Friday" %}",
saturday: "{% trans "Saturday" %}",
sunday: "{% trans "Sunday" %}",
},
playerStatusWindow: "{% trans "Player Status Window" %}",
VNCtoThisDisplay: "{% trans "VNC to this Display" %}",
TeamViewertoThisDisplay: "{% trans "TeamViewer to this Display" %}",
WebkeytoThisDisplay: "{% trans "Webkey to this Display" %}",
};
</script>
{# Add page source code bundle ( JS ) #}
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
{% endblock %}

View File

@@ -1,124 +0,0 @@
{#
OTS Signage Modern Theme - Displays Page Override
Two-column layout with folder panel on left, modern display table
#}
{% extends "authed.twig" %}
{% block pageTitle %}Displays{% endblock %}
{% block content %}
<div class="ots-theme two-column-layout">
<aside class="left-panel displays-sidebar">
<div class="panel-header">
<h3>Folders</h3>
<button class="btn-icon-sm" aria-label="Expand/collapse">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="12 3 20 9 12 15 4 9 12 3"/><polyline points="4 15 12 21 20 15"/>
</svg>
</button>
</div>
<div class="folder-tree">
<div class="folder-item active">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">All Items</span>
</div>
<div class="folder-item">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">Root Folder</span>
</div>
<div class="folder-item">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">TEMPLATE_DemoHolder</span>
</div>
<div class="folder-item" style="margin-left: 16px;">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">Hospitality</span>
</div>
<div class="folder-item" style="margin-left: 16px;">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">Retail</span>
</div>
<div class="folder-item">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">OTS Signs Internal</span>
</div>
</div>
</aside>
<main class="content-panel">
<div class="page-header">
<h1>Displays</h1>
<p class="text-muted">Manage and monitor your digital signage displays</p>
</div>
<div class="content-toolbar">
<div class="search-wrapper">
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input type="search" placeholder="Search displays…" class="form-control search-field" />
</div>
<div class="toolbar-actions">
<button class="btn btn-outline btn-sm">Columns</button>
<a href="{{ baseUrl }}/display/add" class="btn btn-primary btn-sm">Add Display</a>
</div>
</div>
<div class="stat-row">
<div class="stat-box">
<div class="stat-label">Total</div>
<div class="stat-value">1</div>
</div>
<div class="stat-box">
<div class="stat-label">Online</div>
<div class="stat-value text-success">1</div>
</div>
<div class="stat-box">
<div class="stat-label">Offline</div>
<div class="stat-value text-danger">0</div>
</div>
</div>
<div class="table-wrapper">
<table class="table table-striped">
<thead>
<tr>
<th style="width: 25%;">Display</th>
<th style="width: 15%;">Status</th>
<th style="width: 20%;">Folder</th>
<th style="width: 15%;">Group</th>
<th style="width: 15%;">Last Check-in</th>
<th style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Test1</strong></td>
<td><span class="badge badge-success">Online</span></td>
<td>TEMPLATE_DemoHolder</td>
<td>Test Screens</td>
<td><span class="text-xs">just now</span></td>
<td>
<button class="btn-icon-sm" aria-label="Actions">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>
</div>
{% endblock %}

View File

@@ -1,129 +0,0 @@
{#
OTS Signage Modern Theme - Media Library Page Override
Two-column layout with folder panel on left, media grid on right
#}
{% extends "authed.twig" %}
{% block pageTitle %}Media Library{% endblock %}
{% block content %}
<div class="ots-theme two-column-layout">
<aside class="left-panel media-sidebar">
<div class="panel-header">
<h3>Folders</h3>
<button class="btn-icon-sm" aria-label="New folder">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
</div>
<div class="folder-tree">
<div class="folder-item active">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">All Files</span>
</div>
<div class="folder-item">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="folder-name">Root Folder</span>
</div>
<div class="folder-item">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
</svg>
<span class="folder-name">Images</span>
</div>
<div class="folder-item">
<svg class="folder-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
</svg>
<span class="folder-name">Videos</span>
</div>
</div>
</aside>
<main class="content-panel">
<div class="page-header">
<h1>Media Library</h1>
<p class="text-muted">Upload and manage your images and videos for digital signage</p>
</div>
<div class="content-toolbar">
<div class="search-wrapper">
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input type="search" placeholder="Search media…" class="form-control search-field" />
</div>
<div class="toolbar-actions">
<button class="btn btn-outline btn-sm">All Media</button>
<a href="{{ baseUrl }}/library/add" class="btn btn-primary btn-sm">Upload Media</a>
</div>
</div>
<div class="stat-row">
<div class="stat-box">
<div class="stat-label">Total Files</div>
<div class="stat-value">4</div>
</div>
<div class="stat-box">
<div class="stat-label">Storage Used</div>
<div class="stat-value">12.3 MB</div>
</div>
<div class="stat-box">
<div class="stat-label">Storage Limit</div>
<div class="stat-value">5 GB</div>
</div>
</div>
<div class="media-grid">
<div class="media-item">
<div class="media-thumbnail">
<img src="https://images.unsplash.com/photo-1444080748397-f442aa95c3e5?w=400&h=300&fit=crop" alt="Galaxy space" />
<span class="media-type-badge">Image</span>
</div>
<div class="media-info">
<p class="media-name">2000x1158</p>
<p class="media-size text-xs text-muted">3.3 MB • 1920x1112</p>
</div>
</div>
<div class="media-item">
<div class="media-thumbnail">
<img src="https://images.unsplash.com/photo-1478098711619-69891b0ec21a?w=400&h=300&fit=crop" alt="Cat portrait" />
<span class="media-type-badge">Image</span>
</div>
<div class="media-info">
<p class="media-name">Images.jpg</p>
<p class="media-size text-xs text-muted">5.2 KB • 194x260</p>
</div>
</div>
<div class="media-item">
<div class="media-thumbnail">
<img src="https://images.unsplash.com/photo-1577720643272-265b434c829c?w=400&h=300&fit=crop" alt="OTS Logo" />
<span class="media-type-badge">Image</span>
</div>
<div class="media-info">
<p class="media-name">OTS Logo</p>
<p class="media-size text-xs text-muted">2.9 KB • 360x350</p>
</div>
</div>
<div class="media-item">
<div class="media-thumbnail">
<img src="https://images.unsplash.com/photo-1590080876-8b7f22b5d5fa?w=400&h=300&fit=crop" alt="Sunrise Hotel" />
<span class="media-type-badge">Image</span>
</div>
<div class="media-info">
<p class="media-name">suncrest hotel l...</p>
<p class="media-size text-xs text-muted">4.1 KB • 5824x3401</p>
</div>
</div>
</div>
</main>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
{#
OTS Signage Theme override
Optional dashboard message block included with ignore missing
#}
<div class="ots-dashboard-message">
<div class="ots-dashboard-message__title">OTS Theme Active</div>
<div class="ots-dashboard-message__body">This is a low-risk override for troubleshooting. Remove or restyle at any time.</div>
</div>

View File

@@ -0,0 +1,22 @@
{#
OTS Signage Theme - JavaScript and CSS injection
This file is auto-included by Xibo's base.twig at the end of the document
NOTE: CSS and JS are INLINED to bypass web server MIME type issues with /custom/ paths
This ensures all styles and scripts load regardless of web server routing configuration
#}
<!-- Theme CSS overrides - INLINED to bypass MIME type issues -->
<style nonce="{{ cspNonce }}">
{% include "override-styles.twig" %}
</style>
<!-- DataTables contrast fixes - INLINED to override core DataTables defaults -->
<style nonce="{{ cspNonce }}">
{% include "datatable-contrast.twig" %}
</style>
<!-- Theme JavaScript - INLINED to bypass MIME type issues -->
<script nonce="{{ cspNonce }}">
{% include "theme-scripts.twig" %}
</script>

View File

@@ -0,0 +1,368 @@
/**
* OTS Signage Modern Theme - Client-Side Utilities
* Sidebar toggle, dropdown menus, and UI interactions
*/
(function() {
'use strict';
const STORAGE_KEYS = {
sidebarCollapsed: 'otsTheme:sidebarCollapsed'
};
/**
* Initialize sidebar toggle functionality
*/
function initSidebarToggle() {
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
const sidebar = document.querySelector('.ots-sidebar');
if (!toggleBtn || !sidebar) return;
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
sidebar.classList.toggle('active');
});
// Close sidebar when clicking outside on mobile
document.addEventListener('click', function(e) {
if (window.innerWidth <= 768) {
const isClickInsideSidebar = sidebar.contains(e.target);
const isClickOnToggle = toggleBtn.contains(e.target);
if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
sidebar.classList.remove('active');
}
}
});
}
/**
* Initialize dropdown menus
*/
function initDropdowns() {
const dropdowns = document.querySelectorAll('.dropdown');
dropdowns.forEach(dropdown => {
const button = dropdown.querySelector('.dropdown-menu');
if (!button) return;
const menu = dropdown.querySelector('.dropdown-menu');
// Toggle menu on button click
dropdown.addEventListener('click', function(e) {
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]')) {
e.preventDefault();
dropdown.classList.toggle('active');
}
});
// Close menu when clicking outside
document.addEventListener('click', function(e) {
if (!dropdown.contains(e.target)) {
dropdown.classList.remove('active');
}
});
});
}
/**
* Initialize search functionality
*/
function initSearch() {
const searchForm = document.querySelector('.topbar-search');
if (!searchForm) return;
const input = searchForm.querySelector('.search-input');
if (input) {
input.addEventListener('focus', function() {
searchForm.style.borderColor = 'var(--color-primary)';
});
input.addEventListener('blur', function() {
searchForm.style.borderColor = 'var(--color-border)';
});
}
}
/**
* Initialize page specific interactions
*/
function initPageInteractions() {
// Displays page - folder selection
const folderItems = document.querySelectorAll('.folder-item');
folderItems.forEach(item => {
item.addEventListener('click', function() {
folderItems.forEach(f => f.classList.remove('active'));
this.classList.add('active');
});
});
// Filter collapse toggle
const filterCollapseBtn = document.querySelector('#ots-filter-collapse-btn');
const filterContent = document.querySelector('#ots-filter-content');
if (filterCollapseBtn && filterContent) {
let isCollapsed = false;
filterCollapseBtn.addEventListener('click', function() {
isCollapsed = !isCollapsed;
filterContent.classList.toggle('collapsed', isCollapsed);
// Rotate icon
const icon = filterCollapseBtn.querySelector('i');
icon.classList.toggle('fa-chevron-up');
icon.classList.toggle('fa-chevron-down');
// Save preference to localStorage
localStorage.setItem('ots-filter-collapsed', isCollapsed);
});
// Restore saved preference
const savedState = localStorage.getItem('ots-filter-collapsed');
if (savedState === 'true') {
isCollapsed = true;
filterContent.classList.add('collapsed');
const icon = filterCollapseBtn.querySelector('i');
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
}
}
// Displays page - folder tree toggle layout
const folderToggleBtn = document.querySelector('#folder-tree-select-folder-button');
const folderContainer = document.querySelector('.grid-with-folders-container');
const folderTree = document.querySelector('#grid-folder-filter');
if (folderToggleBtn && folderContainer && folderTree) {
let debounceTimeout;
const syncFolderLayout = () => {
// Check actual visibility using computed styles
const computedStyle = window.getComputedStyle(folderTree);
const isHidden = computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
folderTree.offsetHeight === 0;
console.log('Folder collapse sync:', {
isHidden,
display: computedStyle.display,
visibility: computedStyle.visibility,
offsetHeight: folderTree.offsetHeight
});
folderContainer.classList.toggle('ots-folder-collapsed', !!isHidden);
// Log the result
console.log('Container classes:', folderContainer.className);
console.log('Grid template columns:', window.getComputedStyle(folderContainer).gridTemplateColumns);
// Force reflow
folderContainer.offsetHeight;
};
const debouncedSync = () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(syncFolderLayout, 50);
};
// Watch for style/class changes on folderTree (let Xibo's code run first)
const treeObserver = new MutationObserver(() => {
console.log('Folder tree mutation detected, debouncing sync...');
debouncedSync();
});
treeObserver.observe(folderTree, {
attributes: true,
attributeFilter: ['style', 'class']
});
// Initial sync
syncFolderLayout();
// Monitor the folder tree's parent for display changes
const parentObserver = new MutationObserver(debouncedSync);
const treeParent = folderTree.parentElement;
if (treeParent) {
parentObserver.observe(treeParent, {
childList: false,
attributes: true,
subtree: false
});
}
}
// Media page - item selection
const mediaItems = document.querySelectorAll('.media-item');
mediaItems.forEach(item => {
item.addEventListener('click', function() {
this.style.opacity = '0.7';
setTimeout(() => this.style.opacity = '1', 200);
});
});
}
/**
* Make sidebar responsive
*/
function makeResponsive() {
const sidebar = document.querySelector('.ots-sidebar');
const main = document.querySelector('.ots-main');
if (!sidebar) return;
// Add toggle button for mobile
if (window.innerWidth <= 768) {
sidebar.classList.add('mobile');
}
window.addEventListener('resize', function() {
if (window.innerWidth > 768) {
sidebar.classList.remove('mobile', 'active');
} else {
sidebar.classList.add('mobile');
}
});
}
/**
* Prevent Chart.js errors when chart elements are missing
*/
function initChartSafeguard() {
if (!window.Chart) return;
if (typeof window.Chart.acquireContext === 'function') {
window.Chart.acquireContext = function(item) {
if (!item) return null;
const candidate = item.length ? item[0] : item;
if (candidate && typeof candidate.getContext === 'function') {
return candidate.getContext('2d');
}
return null;
};
return;
}
if (window.Chart.prototype && typeof window.Chart.prototype.acquireContext === 'function') {
window.Chart.prototype.acquireContext = function(item) {
if (!item) return null;
const candidate = item.length ? item[0] : item;
if (candidate && typeof candidate.getContext === 'function') {
return candidate.getContext('2d');
}
return null;
};
}
}
/**
* Enhance tables: wrap in card, add per-table search box, client-side filtering
* Non-destructive: skips tables already enhanced
*/
function enhanceTables() {
const selector = '.ots-content table, .content table, .container table, .card table, table';
const tables = Array.from(document.querySelectorAll(selector));
let counter = 0;
tables.forEach(table => {
// only enhance tables that have a thead and tbody
if (!table || table.classList.contains('modern-table')) return;
if (!table.querySelector('thead') || !table.querySelector('tbody')) return;
counter += 1;
table.classList.add('modern-table');
// Build wrapper structure
const wrapper = document.createElement('div');
wrapper.className = 'modern-table-card';
const controls = document.createElement('div');
controls.className = 'table-controls';
const input = document.createElement('input');
input.type = 'search';
input.placeholder = 'Search…';
input.className = 'table-search-input';
input.setAttribute('aria-label', 'Table search');
input.style.minWidth = '180px';
controls.appendChild(input);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-wrapper';
tableWrapper.style.overflow = 'auto';
// Insert wrapper into DOM in place of the table
const parent = table.parentNode;
parent.replaceChild(wrapper, table);
wrapper.appendChild(controls);
wrapper.appendChild(tableWrapper);
tableWrapper.appendChild(table);
// Simple, light-weight search filtering for this table only
input.addEventListener('input', function (e) {
const term = (e.target.value || '').toLowerCase();
table.querySelectorAll('tbody tr').forEach(tr => {
const text = tr.textContent.toLowerCase();
tr.style.display = term === '' || text.includes(term) ? '' : 'none';
});
});
});
}
/**
* Initialize DataTables for enhanced behavior when available.
* Falls back gracefully if DataTables or jQuery are not present.
*/
function initDataTables() {
if (!window.jQuery) return;
const $ = window.jQuery;
if (!$.fn || !$.fn.dataTable) return;
// Skip Xibo-managed grids to avoid double initialization
if (document.querySelector('.XiboGrid')) return;
$('.modern-table, table').each(function () {
try {
if (this.closest('.XiboGrid')) return;
if (!$.fn.dataTable.isDataTable(this)) {
$(this).DataTable({
responsive: true,
lengthChange: false,
pageLength: 10,
autoWidth: false,
dom: '<"table-controls"f>rt<"table-meta"ip>',
language: { search: '' }
});
}
} catch (err) {
// If initialization fails, ignore and allow fallback enhancer
console.warn('DataTables init failed for table', this, err);
}
});
}
/**
* Initialize all features when DOM is ready
*/
function init() {
initSidebarToggle();
initDropdowns();
initSearch();
initPageInteractions();
initDataTables();
enhanceTables();
makeResponsive();
initChartSafeguard();
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

369
display-page.twig Normal file
View File

@@ -0,0 +1,369 @@
{#
/**
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{{ "Displays"|trans }} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
{% if currentUser.featureEnabled("displays.add") %}
<button class="btn btn-success XiboFormButton" title="{% trans "Add a Display via user_code displayed on the Player screen" %}" href="{{ url_for("display.addViaCode.form") }}"> <i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Display (Code)" %}</button>
{% endif %}
<button class="btn btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
{% endblock %}
{% block headContent %}
{# Add page source code bundle ( CSS ) #}
<link rel="stylesheet" href="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.css?v={{ version }}&rev={{revision }}">
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">{% trans "Displays" %}</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayView">
<div class="XiboFilter card mb-3 bg-light">
<div class="FilterDiv card-body" id="Filter">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a class="nav-link active" href="#filter-general" role="tab" data-toggle="tab">{% trans "General" %}</a></li>
<li class="nav-item"><a class="nav-link" href="#filter-advanced" role="tab" data-toggle="tab">{% trans "Advanced" %}</a></li>
</ul>
<form class="form-inline">
<div class="tab-content">
<div class="tab-pane active" id="filter-general">
{% set title %}{% trans "ID" %}{% endset %}
{{ inline.number("displayId", title) }}
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('display', title) }}
{% set title %}{% trans "Status" %}{% endset %}
{% set check %}{% trans "Up to date" %}{% endset %}
{% set cross %}{% trans "Downloading" %}{% endset %}
{% set cloud %}{% trans "Out of date" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: check},
{ optionid: "2", option: cross},
{ optionid: "3", option: cloud}
] %}
{{ inline.dropdown("mediaInventoryStatus", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Logged In?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: yesOption},
{ optionid: "0", option: noOption}
] %}
{{ inline.dropdown("loggedIn", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Authorised?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: yesOption },
{ optionid: "0", option: noOption},
] %}
{{ inline.dropdown("authorised", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "XMR Registered?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: 1, option: yesOption},
{ optionid: 0, option: noOption},
] %}
{{ inline.dropdown("xmrRegistered", "single", title, "", options, "optionid", "option") }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% if currentUser.featureEnabled("displaygroup.view") %}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-filter-options", value: '{"isDisplaySpecific":0}' },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" },
{ name: "data-initial-key", value: "displayGroupId" },
] %}
{{ inline.dropdown("displayGroupId", "single", title, "", null, "displayGroupId", "displayGroup", helpText, "pagedSelect", "", "", "", attributes) }}
{% endif %}
{% if currentUser.featureEnabled("displayprofile.view") %}
{% set title %}{% trans "Display Profile" %}{% endset %}
{{ inline.dropdown("displayProfileId", "single", title, "", [{displayProfileId:null, name:""}]|merge(displayProfiles), "displayProfileId", "name") }}
{% endif %}
{{ inline.hidden("folderId") }}
</div>
<div class="tab-pane" id="filter-advanced">
{% set title %}{% trans "Last Accessed" %}{% endset %}
{{ inline.date("lastAccessed", title) }}
{% set title %}{% trans "Player Type" %}{% endset %}
{% set android %}{% trans "Android" %}{% endset %}
{% set chromeos %}{% trans "ChromeOS" %}{% endset %}
{% set windows %}{% trans "Windows" %}{% endset %}
{% set webos %}{% trans "webOS" %}{% endset %}
{% set sssp %}{% trans "Tizen" %}{% endset %}
{% set linux %}{% trans "Linux" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "android", option: android},
{ optionid: "chromeos", option: chromeos},
{ optionid: "windows", option: windows},
{ optionid: "lg", option: webos},
{ optionid: "sssp", option: sssp},
{ optionid: "linux", option: linux},
] %}
{{ inline.dropdown("clientType", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Player Code" %}{% endset %}
{{ inline.input("clientCode", title) }}
{% set title %}{% trans "Custom ID" %}{% endset %}
{{ inline.input("customId", title) }}
{% set title %}{% trans "Mac Address" %}{% endset %}
{{ inline.input("macAddress", title) }}
{% set title %}{% trans "IP Address" %}{% endset %}
{{ inline.input("clientAddress", title) }}
{% set title %}{% trans "Orientation" %}{% endset %}
{% set landscape %}{% trans "Landscape" %}{% endset %}
{% set portrait %}{% trans "Portrait" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "landscape", option: landscape},
{ optionid: "portrait", option: portrait}
] %}
{{ inline.dropdown("orientation", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Commercial Licence" %}{% endset %}
{% set licensed %}{% trans "Licensed fully" %}{% endset %}
{% set trial %}{% trans "Trial" %}{% endset %}
{% set notLinceced %}{% trans "Not licenced" %}{% endset %}
{% set notApplicable %}{% trans "Not applicable" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: "1", option: licensed},
{ optionid: "2", option: trial},
{ optionid: "0", option: notLinceced},
{ optionid: "3", option: notApplicable}
] %}
{{ inline.dropdown("commercialLicence", "single", title, "", options, "optionid", "option") }}
{% set title %}{% trans "Player supported?" %}{% endset %}
{% set yesOption %}{% trans "Yes" %}{% endset %}
{% set noOption %}{% trans "No" %}{% endset %}
{% set options = [
{ optionid: "", option: "" },
{ optionid: 1, option: yesOption},
{ optionid: 0, option: noOption},
] %}
{{ inline.dropdown("isPlayerSupported", "single", title, "", options, "optionid", "option") }}
</div>
</div>
</form>
</div>
</div>
<div class="grid-with-folders-container">
<div class="grid-folder-tree-container p-3" id="grid-folder-filter">
<input id="jstree-search" class="form-control" type="text" placeholder="{% trans "Search" %}">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="folder-tree-clear-selection-button">
<label class="form-check-label" for="folder-tree-clear-selection-button" title="{% trans "Search in all folders" %}">{% trans "All Folders" %}</label>
</div>
<div class="folder-search-no-results d-none">
<p>{% trans 'No Folders matching the search term' %}</p>
</div>
<div id="container-folder-tree"></div>
</div>
<div class="folder-controller d-none">
<button type="button" id="folder-tree-select-folder-button" class="btn btn-outline-secondary" title="{{ "Open / Close Folder Search options"|trans }}"><i class="fas fa-folder fa-1x"></i></button>
<div id="breadcrumbs" class="mt-2 pl-2"></div>
</div>
<div class="map-controller d-none pl-1">
<button type="button" id="map_button" class="btn btn-primary" title="{{ "Map"|trans }}"><i class="fa fa-map"></i></button>
</div>
<div class="list-controller d-none pl-1">
<button type="button" id="list_button" class="btn btn-primary" title="{{ "List"|trans }}"><i class="fa fa-list"></i></button>
</div>
<div id="datatable-container">
<div class="XiboData card py-3">
<table id="displays" class="table table-striped" data-content-type="display" data-content-id-name="displayId" data-state-preference-name="displayGrid" style="width: 100%;">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Display Type" %}</th>
<th>{% trans "Address" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Authorised?" %}</th>
<th>{% trans "Current Layout" %}</th>
<th>{% trans "Storage Available" %}</th>
<th>{% trans "Storage Total" %}</th>
<th>{% trans "Storage Free %" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Orientation" %}</th>
<th>{% trans "Resolution" %}</th>
{% if currentUser.featureEnabled("tag.tagging") %}<th>{% trans "Tags" %}</th>{% endif %}
<th>{% trans "Default Layout" %}</th>
<th>{% trans "Interleave Default" %}</th>
<th>{% trans "Email Alert" %}</th>
<th>{% trans "Logged In" %}</th>
<th>{% trans "Last Accessed" %}</th>
<th>{% trans "Display Profile" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Supported?" %}</th>
<th>{% trans "Device Name" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Mac Address" %}</th>
<th>{% trans "Timezone" %}</th>
<th>{% trans "Languages" %}</th>
<th>{% trans "Latitude" %}</th>
<th>{% trans "Longitude" %}</th>
<th>{% trans "Screen shot?" %}</th>
<th>{% trans "Thumbnail" %}</th>
<th>{% trans "CMS Transfer?" %}</th>
<th>{% trans "Bandwidth Limit" %}</th>
<th>{% trans "Last Command" %}</th>
<th>{% trans "XMR Registered" %}</th>
<th>{% trans "Commercial Licence" %}</th>
<th>{% trans "Remote" %}</th>
<th>{% trans "Sharing" %}</th>
<th>{% trans "Screen Size" %}</th>
<th>{% trans "Is Mobile?" %}</th>
<th>{% trans "Outdoor?" %}</th>
<th>{% trans "Reference 1" %}</th>
<th>{% trans "Reference 2" %}</th>
<th>{% trans "Reference 3" %}</th>
<th>{% trans "Reference 4" %}</th>
<th>{% trans "Reference 5" %}</th>
<th>{% trans "Custom ID" %}</th>
<th>{% trans "Cost Per Play" %}</th>
<th>{% trans "Impressions Per Play" %}</th>
<th>{% trans "Created Date" %}</th>
<th>{% trans "Modified Date" %}</th>
<th>{% trans "Faults?" %}</th>
<th>{% trans "OS Version" %}</th>
<th>{% trans "OS SDK" %}</th>
<th>{% trans "Manufacturer" %}</th>
<th>{% trans "Brand" %}</th>
<th>{% trans "Model" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!-- Map -->
<div class="row">
<div class="col-sm-12">
<div class="map-legend" style="display:none; position: absolute; z-index: 500; right: 20px; top: 10px;">
<div class="display-map-legend" style="font-size: 12px;">
<div>Logged in</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-green-check.png'/> - Up to date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-yellow-check.png'/> - Out of date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-red-check.png'/> - Downloading/Unknown</div>
</br>
<div>Logged out</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-green-cross.png'/> - Up to date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-yellow-cross.png'/> - Out of date</div>
<div><img style="width: 15%" src='{{ theme.rootUri() }}dist/assets/map-marker-red-cross.png'/> - Downloading/Unknown</div>
</div>
</div>
<div id="display-map" data-displays-url="{{ url_for("display.map") }}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
{# Initialise JS variables and translations #}
<script type="text/javascript" nonce="{{ cspNonce }}" defer>
{# JS variables #}
var publicPath = "{{ theme.rootUri() }}";
var displaySearchURL = "{{ url_for('display.search') }}";
var layoutSearchURL = "{{ url_for('layout.search') }}";
var mapConfig = {{ mapConfig| json_encode | raw }};
var playerVersionSupport = "{{playerVersion}}";
var folderViewEnabled = "{{ currentUser.featureEnabled('folder.view') }}";
var taggingEnabled = "{{ currentUser.featureEnabled('tag.tagging') }}";
var showThumbnailColumn = "{{ currentUser.getOptionValue('showThumbnailColumn', 1) }}";
var SHOW_DISPLAY_AS_VNCLINK = "{{ settings.SHOW_DISPLAY_AS_VNCLINK }}";
var SHOW_DISPLAY_AS_VNC_TGT = "{{ settings.SHOW_DISPLAY_AS_VNC_TGT }}";
{# Custom translations #}
var displayPageTrans = {
back: "{% trans "Back" %}",
yes: "{% trans "Yes" %}",
no: "{% trans "No" %}",
daysOfTheWeek: {
monday: "{% trans "Monday" %}",
tuesday: "{% trans "Tuesday" %}",
wednesday: "{% trans "Wednesday" %}",
thursday: "{% trans "Thursday" %}",
friday: "{% trans "Friday" %}",
saturday: "{% trans "Saturday" %}",
sunday: "{% trans "Sunday" %}",
},
playerStatusWindow: "{% trans "Player Status Window" %}",
VNCtoThisDisplay: "{% trans "VNC to this Display" %}",
TeamViewertoThisDisplay: "{% trans "TeamViewer to this Display" %}",
WebkeytoThisDisplay: "{% trans "Webkey to this Display" %}",
};
</script>
{# Add page source code bundle ( JS ) #}
<script src="{{ theme.rootUri() }}dist/leaflet.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
<script src="{{ theme.rootUri() }}dist/pages/display-page.bundle.min.js?v={{ version }}&rev={{revision}}" nonce="{{ cspNonce }}"></script>
{% endblock %}