Redesigns themes page

main
Yasen Pramatarov 2025-11-26 19:28:25 +02:00
parent 251cfa35f3
commit 969875460f
4 changed files with 847 additions and 47 deletions

View File

@ -284,6 +284,115 @@ EOT;
} }
/**
* Get descriptive metadata for a theme.
*
* @param string $themeId
* @return array{name:string,description:string,version:string,author:string,tags:array}
*/
public static function getThemeMetadata(string $themeId): array
{
static $cache = [];
if (isset($cache[$themeId])) {
return $cache[$themeId];
}
$config = self::getConfig();
$defaults = $config['default_config'] ?? [];
$availableEntry = $config['available_themes'][$themeId] ?? null;
$metadata = [
'name' => is_array($availableEntry) ? ($availableEntry['name'] ?? ucfirst($themeId)) : ($availableEntry ?? ucfirst($themeId)),
'description' => $defaults['description'] ?? '',
'version' => $defaults['version'] ?? '',
'author' => $defaults['author'] ?? '',
'tags' => [],
'type' => $themeId === 'default' ? 'Core built-in' : 'Custom',
'path' => $themeId === 'default' ? 'app/templates' : ('themes/' . $themeId),
'last_modified' => null,
'file_count' => null
];
if (is_array($availableEntry)) {
$metadata = array_merge($metadata, array_intersect_key($availableEntry, array_flip(['name', 'description', 'version', 'author', 'tags'])));
}
if ($themeId !== 'default') {
$themesDir = rtrim($config['paths']['themes'] ?? (__DIR__ . '/../../themes'), '/');
$themeConfigPath = $themesDir . '/' . $themeId . '/config.php';
if (file_exists($themeConfigPath)) {
$themeConfig = require $themeConfigPath;
if (is_array($themeConfig)) {
$metadata = array_merge($metadata, array_intersect_key($themeConfig, array_flip(['name', 'description', 'version', 'author', 'tags'])));
}
}
}
if (empty($metadata['description'])) {
$metadata['description'] = $defaults['description'] ?? 'A Jilo Web theme';
}
if (empty($metadata['version'])) {
$metadata['version'] = $defaults['version'] ?? '1.0.0';
}
if (empty($metadata['author'])) {
$metadata['author'] = $defaults['author'] ?? 'Lindeas';
}
if (empty($metadata['tags']) || !is_array($metadata['tags'])) {
$metadata['tags'] = [];
}
$paths = $config['paths'] ?? [];
if ($themeId === 'default') {
$absolutePath = realpath($paths['templates'] ?? (__DIR__ . '/../templates')) ?: null;
} else {
$absolutePath = self::getThemePath($themeId);
}
if ($absolutePath && is_dir($absolutePath)) {
[$lastModified, $fileCount] = self::getDirectoryStats($absolutePath);
if ($lastModified !== null) {
$metadata['last_modified'] = $lastModified;
}
if ($fileCount > 0) {
$metadata['file_count'] = $fileCount;
}
}
return $cache[$themeId] = $metadata;
}
/**
* Calculate directory statistics for a theme folder.
*/
private static function getDirectoryStats(string $path): array
{
$latest = null;
$count = 0;
try {
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$count++;
$mtime = $fileInfo->getMTime();
if ($latest === null || $mtime > $latest) {
$latest = $mtime;
}
}
} catch (\Throwable $e) {
return [null, 0];
}
return [$latest, $count];
}
/** /**
* Get the URL for a theme asset * Get the URL for a theme asset
* *

View File

@ -51,11 +51,20 @@ if (isset($_GET['switch_to'])) {
$themes = \App\Helpers\Theme::getAvailableThemes(); $themes = \App\Helpers\Theme::getAvailableThemes();
$currentTheme = \App\Helpers\Theme::getCurrentThemeName(); $currentTheme = \App\Helpers\Theme::getCurrentThemeName();
// Prepare theme data with screenshot URLs for the view // Prepare theme data with screenshot URLs and metadata for the view
$themeData = []; $themeData = [];
foreach ($themes as $id => $name) { foreach ($themes as $id => $name) {
$meta = \App\Helpers\Theme::getThemeMetadata($id);
$themeData[$id] = [ $themeData[$id] = [
'name' => $name, 'name' => $meta['name'] ?? $name,
'description' => $meta['description'] ?? '',
'version' => $meta['version'] ?? '',
'author' => $meta['author'] ?? '',
'tags' => $meta['tags'] ?? [],
'type' => $meta['type'] ?? '',
'path' => $meta['path'] ?? '',
'last_modified' => $meta['last_modified'] ?? null,
'file_count' => $meta['file_count'] ?? null,
'screenshotUrl' => \App\Helpers\Theme::getAssetUrl($id, 'screenshot.png'), 'screenshotUrl' => \App\Helpers\Theme::getAssetUrl($id, 'screenshot.png'),
'isActive' => $id === $currentTheme 'isActive' => $id === $currentTheme
]; ];

View File

@ -10,35 +10,124 @@
* - isActive: Whether this is the current theme * - isActive: Whether this is the current theme
*/ */
?> ?>
<div class="container mt-4"> <?php
<h2>Theme switcher</h2> $activeThemeName = 'Default';
<p class="text-muted">Select a theme to change the appearance of the application.</p> foreach ($themes as $themeData) {
<div class="row mt-4"> if (!empty($themeData['isActive'])) {
<?php foreach ($themes as $themeId => $theme): ?> $activeThemeName = $themeData['name'];
<div class="col-md-4 mb-4"> break;
<div class="card h-100 <?= $theme['isActive'] ? 'border-primary' : '' ?>"> }
<!-- Theme screenshot --> }
<div class="theme-screenshot" style="height: 150px; background-size: cover; background-position: center; background-color: #f8f9fa; <?= $theme['screenshotUrl'] ? 'background-image: url(' . htmlspecialchars($theme['screenshotUrl']) . ')' : '' ?>"> $totalThemes = count($themes);
<?php if (!$theme['screenshotUrl']): ?> ?>
<div class="h-100 d-flex align-items-center justify-content-center text-muted">No preview available</div>
<?php endif; ?> <section class="tm-directory tm-theme-directory">
<div class="tm-hero-card tm-hero-card--stacked">
<div class="tm-hero-head">
<div class="tm-hero-body">
<div class="tm-hero-heading">
<h1 class="tm-hero-title">Themes</h1>
<p class="tm-hero-subtitle">Personalize <?= htmlspecialchars($config['site_name']); ?> with custom visual styles.</p>
</div> </div>
<?php if ($theme['isActive']): ?> <div class="tm-hero-meta">
<div class="card-header bg-primary text-white">Current theme</div> <span class="tm-hero-pill pill-neutral">
<?php endif; ?> <i class="fas fa-layer-group"></i>
<div class="card-body d-flex flex-column"> <?= $totalThemes ?> available
<h5 class="card-title"><?= htmlspecialchars($theme['name']) ?></h5> </span>
<p class="card-text text-muted">Theme ID: <code><?= htmlspecialchars($themeId) ?></code></p> <span class="tm-hero-pill pill-primary">
<div class="mt-auto"> <i class="fas fa-check-circle"></i>
<?php if (!$theme['isActive']): ?> Active: <?= htmlspecialchars($activeThemeName) ?>
<a href="?page=theme&switch_to=<?= urlencode($themeId) ?>&csrf_token=<?= $csrf_token ?>" class="btn btn-primary">Switch to this theme</a> </span>
<?php else: ?>
<button class="btn btn-outline-secondary" disabled>Currently active</button>
<?php endif; ?>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<?php endforeach; ?>
<div class="tm-theme-gallery">
<div class="tm-theme-grid">
<?php foreach ($themes as $themeId => $theme):
$isActive = !empty($theme['isActive']);
$screenshot = $theme['screenshotUrl'];
?>
<article class="tm-theme-card<?= $isActive ? ' is-active' : '' ?>">
<div class="tm-theme-preview" style="<?= $screenshot ? 'background-image: url(' . htmlspecialchars($screenshot) . ')' : '' ?>">
<?php if (!$screenshot): ?>
<span>No preview available</span>
<?php endif; ?>
</div>
<div class="tm-theme-body">
<div class="tm-theme-heading">
<p class="tm-theme-id">ID: <code><?= htmlspecialchars($themeId) ?></code></p>
<h3 class="tm-theme-name"><?= htmlspecialchars($theme['name']) ?></h3>
</div>
<?php if (!empty($theme['description'])): ?>
<p class="tm-theme-description">
<?= htmlspecialchars($theme['description']) ?>
</p>
<?php endif; ?>
<dl class="tm-theme-meta">
<?php if (!empty($theme['version'])): ?>
<div class="tm-theme-meta-item">
<dt>Version</dt>
<dd><?= htmlspecialchars($theme['version']) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($theme['author'])): ?>
<div class="tm-theme-meta-item">
<dt>Author</dt>
<dd><?= htmlspecialchars($theme['author']) ?></dd>
</div>
<?php endif; ?>
</dl>
<?php if (!empty($theme['tags'])): ?>
<ul class="tm-theme-tags">
<?php foreach ($theme['tags'] as $tag): $tagLabel = trim((string)$tag); if ($tagLabel === '') { continue; } ?>
<li><?= htmlspecialchars($tagLabel) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<dl class="tm-theme-stats">
<?php if (!empty($theme['type'])): ?>
<div class="tm-theme-stat">
<dt>Type</dt>
<dd><?= htmlspecialchars($theme['type']) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($theme['file_count'])): ?>
<div class="tm-theme-stat">
<dt>Files</dt>
<dd><?= number_format((int)$theme['file_count']) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($theme['path'])): ?>
<div class="tm-theme-stat">
<dt>Location</dt>
<dd><code><?= htmlspecialchars($theme['path']) ?></code></dd>
</div>
<?php endif; ?>
<?php if (!empty($theme['last_modified'])):
$lastEdited = is_numeric($theme['last_modified']) ? date('M j, Y', (int)$theme['last_modified']) : $theme['last_modified'];
?>
<div class="tm-theme-stat">
<dt>Last edit</dt>
<dd><?= htmlspecialchars($lastEdited) ?></dd>
</div>
<?php endif; ?>
</dl>
<div class="tm-theme-actions">
<?php if ($isActive): ?>
<button class="btn btn-outline-secondary" disabled>
<i class="fas fa-check"></i> Active
</button>
<?php else: ?>
<a class="btn btn-primary" href="?page=theme&amp;switch_to=<?= urlencode($themeId) ?>&amp;csrf_token=<?= $csrf_token ?>">
<i class="fas fa-paint-brush"></i> Apply
</a>
<?php endif; ?>
</div> </div>
</div> </div>
</article>
<?php endforeach; ?>
</div>
</div>
</section>

View File

@ -1,3 +1,210 @@
.tm-theme-gallery {
padding: 2rem 0;
}
.tm-theme-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.tm-theme-eyebrow {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.75rem;
color: #94a3b8;
margin-bottom: 0.4rem;
}
.tm-theme-title {
margin: 0;
font-size: 2rem;
font-weight: 600;
color: #0f172a;
}
.tm-theme-subtitle {
margin: 0.5rem 0 0;
color: #475569;
max-width: 520px;
}
.tm-theme-current {
background: rgba(15, 23, 42, 0.03);
border: 1px solid rgba(148, 163, 184, 0.3);
border-radius: 1rem;
padding: 1rem 1.5rem;
display: inline-flex;
flex-direction: column;
min-width: 220px;
}
.tm-theme-current-label {
text-transform: uppercase;
letter-spacing: 0.21em;
font-size: 0.7rem;
color: #94a3b8;
margin-bottom: 0.35rem;
}
.tm-theme-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.tm-theme-card {
background: rgba(255, 255, 255, 0.98);
border-radius: 0.5rem;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 25px 45px rgba(15, 23, 42, 0.06);
overflow: hidden;
display: flex;
flex-direction: column;
}
.tm-theme-card.is-active {
border-color: rgba(37, 99, 235, 0.5);
box-shadow: 0 35px 60px rgba(37, 99, 235, 0.15);
}
.tm-theme-preview {
position: relative;
padding-top: 58%;
background-color: rgba(148, 163, 184, 0.15);
background-size: cover;
background-position: center;
}
.tm-theme-preview span {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 0.95rem;
}
.tm-theme-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.tm-theme-heading {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.tm-theme-id {
margin: 0 0 0.25rem;
font-size: 0.85rem;
color: #94a3b8;
}
.tm-theme-name {
margin: 0;
font-size: 1.25rem;
color: #0f172a;
}
.tm-theme-description {
margin: 0;
color: #475569;
font-size: 0.95rem;
line-height: 1.5;
}
.tm-theme-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin: 0;
padding: 0;
}
.tm-theme-meta-item {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.tm-theme-meta-item dt {
margin: 0;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: #94a3b8;
}
.tm-theme-meta-item dd {
margin: 0;
font-weight: 600;
color: #0f172a;
}
.tm-theme-tags {
list-style: none;
padding: 0;
margin: -0.25rem 0 0.25rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tm-theme-tags li {
background: rgba(37, 99, 235, 0.08);
color: #1d4ed8;
font-size: 0.8rem;
padding: 0.25rem 0.65rem;
border-radius: 999px;
}
.tm-theme-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.25rem;
/* margin: 0.25rem 0 0;*/
margin: 0;
padding: 0;
}
.tm-theme-stat {
background: rgba(15, 23, 42, 0.03);
border: 1px solid rgba(148, 163, 184, 0.25);
border-radius: 0.5rem;
/* padding: 0.75rem 1rem;*/
padding: 0.3rem;
display: flex;
flex-direction: column;
/* gap: 0.2rem;*/
}
.tm-theme-stat dt {
margin: 0;
font-size: 0.7rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #94a3b8;
}
.tm-theme-stat dd {
margin: 0;
font-weight: 600;
color: #0f172a;
font-size: 0.7rem;
text-transform: uppercase;
}
.tm-theme-actions .btn {
width: 100%;
}
html, body { html, body {
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;
@ -86,6 +293,46 @@ html, body {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
.tm-hero-controls {
display: inline-flex;
gap: 0.5rem;
margin-top: 0.85rem;
background: rgba(148, 163, 184, 0.15);
padding: 0.25rem;
border-radius: 999px;
align-self: flex-start;
}
.tm-toggle-pill {
border: none;
background: transparent;
color: #475569;
font-size: 0.9rem;
font-weight: 500;
padding: 0.4rem 1.05rem;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 0.35rem;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.tm-toggle-pill i {
font-size: 0.85rem;
}
.tm-toggle-pill:is(:hover, :focus-visible) {
background: rgba(255, 255, 255, 0.7);
color: #0f172a;
}
.tm-toggle-pill.is-active {
background: #1d4ed8;
color: #fff;
box-shadow: 0 10px 25px rgba(30, 64, 175, 0.25);
}
.tm-hero-pill { .tm-hero-pill {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -504,18 +751,40 @@ html, body {
/* Dashboard widgets */ /* Dashboard widgets */
.tm-widget-card { .tm-widget-card {
background: rgba(255, 255, 255, 0.97); background: rgba(255, 255, 255, 0.97);
border-radius: 1.25rem; border-radius: 1rem;
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08); box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08);
padding: 1.75rem; /* padding: 1.75rem;*/
margin-bottom: 1.75rem; margin-bottom: 1.75rem;
overflow: hidden;
} }
.tm-widget-header { .tm-widget-header {
display: flex; display: flex;
justify-content: space-between; align-items: center;
align-items: flex-start; /* gap: 0.85rem;*/
gap: 1rem; background: linear-gradient(135deg, #eef2ff 0%, #e0f2fe 100%);
margin-bottom: 1rem; /* border-radius: 1rem;*/
padding: 0.25rem;
/* margin: -0.25rem -0.25rem 1.25rem;*/
margin: -0.25rem -0.25rem 0rem;
}
.tm-widget-heading {
display: flex;
align-items: center;
/* gap: 0.75rem;*/
}
.tm-widget-title-icon {
width: 48px;
height: 48px;
border-radius: 16px;
/* background: rgba(15, 23, 42, 0.08);*/
display: inline-flex;
align-items: center;
justify-content: center;
color: #0f172a;
font-size: 1.25rem;
} }
.tm-widget-eyebrow { .tm-widget-eyebrow {
@ -531,13 +800,18 @@ html, body {
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 1.35rem; /* font-size: 1.35rem;*/
font-size: 1rem;
font-weight: 600; font-weight: 600;
color: #0f172a; color: #0f172a;
display: flex; display: inline-flex;
align-items: center; align-items: baseline;
gap: 0.5rem; /* gap: 0.4rem;*/
cursor: pointer; cursor: default;
}
.tm-widget-title a {
text-decoration: none;
} }
.tm-widget-title:focus { .tm-widget-title:focus {
@ -583,7 +857,9 @@ html, body {
.tm-widget-body { .tm-widget-body {
border-top: 1px solid rgba(148, 163, 184, 0.2); border-top: 1px solid rgba(148, 163, 184, 0.2);
padding-top: 1rem; /* padding-top: 1rem;*/
padding: 1rem;
/* margin-top: 0.75rem;*/
} }
.tm-widget-table thead { .tm-widget-table thead {
@ -848,6 +1124,7 @@ html, body {
border: 1px solid rgba(148, 163, 184, 0.5); border: 1px solid rgba(148, 163, 184, 0.5);
border-radius: 0.85rem; border-radius: 0.85rem;
padding: 0.55rem 0.85rem; padding: 0.55rem 0.85rem;
height: 100%;
} }
.tm-filter-input input { .tm-filter-input input {
@ -872,9 +1149,51 @@ html, body {
gap: 1rem; gap: 1rem;
flex-wrap: nowrap; flex-wrap: nowrap;
width: 100%; width: 100%;
height: 2.25rem;
/* margin-bottom: 1rem;*/ /* margin-bottom: 1rem;*/
} }
/*.tm-call-filter-row--compact {*/
/* align-items: center;*/
/* gap: 0.65rem;*/
/* height: auto;*/
/*}*/
.tm-call-filter-row--compact .tm-filter-field {
flex: 0 0 auto;
}
.tm-call-filter-row--compact .tm-filter-input {
width: auto;
padding: 0.4rem 0.85rem;
white-space: nowrap;
}
.tm-call-filter-row--compact .tm-filter-input select,
.tm-call-filter-row--compact .tm-filter-input input[type="date"] {
width: auto;
min-width: 0;
flex: 0 0 auto;
}
.tm-call-filter-row--compact .tm-filter-input--tagged {
padding: 0.35rem 0.6rem;
}
.tm-call-filter-row--compact .tm-filter-input--tagged input[type="date"] {
width: auto;
text-transform: uppercase;
font-weight: lighter;
font-variant: all-small-caps;
}
.tm-call-filter-actions--compact {
margin-left: auto;
display: inline-flex;
gap: 0.6rem;
align-items: center;
}
.tm-call-filter-row:last-of-type { .tm-call-filter-row:last-of-type {
margin-bottom: 0; margin-bottom: 0;
} }
@ -884,6 +1203,11 @@ html, body {
min-width: 0; min-width: 0;
} }
.tm-call-filter-row.tm-call-filter-row--compact .tm-filter-field {
flex: 0 0 auto;
min-width: auto;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.tm-call-filter-row { .tm-call-filter-row {
flex-wrap: wrap; flex-wrap: wrap;
@ -902,7 +1226,8 @@ html, body {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 0.2rem 0.6rem; padding: 0.2rem 0.6rem;
border-radius: 999px; /* border-radius: 999px;*/
border-radius: 10px;
font-size: 0.75rem; font-size: 0.75rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
@ -913,18 +1238,52 @@ html, body {
.tm-directory-filter-form .form-select, .tm-directory-filter-form .form-select,
.tm-directory-filter-form select { .tm-directory-filter-form select {
width: 100%; width: 100%;
background: transparent;
border: none;
} }
.tm-directory-filter-form .select2-container { .tm-directory-filter-form .select2-container {
width: 100% !important; width: 100% !important;
} }
.tm-directory-filter-form .form-select:focus,
.tm-directory-filter-form select:focus,
.tm-directory-filter-form .select2-selection:focus,
.tm-directory-filter-form .select2-selection--single:focus,
.tm-directory-filter-form .select2-selection--multiple:focus {
outline: none;
box-shadow: none;
}
.tm-directory-filter-form .select2-selection,
.tm-directory-filter-form .select2-container--default .select2-selection--single,
.tm-directory-filter-form .select2-container--default .select2-selection--multiple {
background-color: transparent !important;
border: none !important;
min-height: auto;
padding-left: 0;
box-shadow: none;
}
.tm-directory-filter-form .select2-selection__rendered {
padding-left: 0;
color: inherit;
}
.tm-directory-filter-form .select2-selection__choice {
background: rgba(37, 99, 235, 0.08);
border: none;
color: #1d4ed8;
}
.tm-filter-actions { .tm-filter-actions {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.tm-directory-meta { .tm-directory-meta {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1036,6 +1395,7 @@ html, body {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.3rem 0.65rem; gap: 0.3rem 0.65rem;
align-items: center; align-items: center;
position: relative;
} }
.tm-directory-field { .tm-directory-field {
@ -1057,6 +1417,227 @@ html, body {
color: #94a3b8; color: #94a3b8;
} }
.tm-directory-match-flag {
position: absolute;
top: 0.4rem;
right: 0.4rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(37, 99, 235, 0.12);
color: #1d4ed8;
font-size: 0.75rem;
}
.tm-notification-list {
display: flex;
flex-direction: column;
gap: 0.3rem;
/* margin-top: 1.5rem;*/
}
.tm-notification-card {
background: #fff;
border: 1px solid rgba(148, 163, 184, 0.25);
border-radius: 0.5rem;
padding: 1.25rem;
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08);
display: flex;
flex-direction: column;
gap: 0.85rem;
position: relative;
width: 100%;
}
.tm-notification-card.is-unread {
border-color: rgba(37, 99, 235, 0.35);
}
.tm-notification-card-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.tm-notification-card-info {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
flex: 1 1 auto;
min-width: 0;
}
.tm-notification-subject {
margin: 0;
font-size: 1rem;
color: #0f172a;
}
.tm-notification-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(148, 163, 184, 0.6);
display: inline-block;
margin-right: 0.45rem;
}
.tm-notification-card.is-unread .tm-notification-status-dot {
background: #1d4ed8;
}
.tm-notification-badge {
border-radius: 999px;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.tm-notification-badge.badge-unread {
background: rgba(37, 99, 235, 0.12);
color: #1d4ed8;
}
.tm-notification-badge.badge-read {
background: rgba(148, 163, 184, 0.15);
color: #475569;
}
.tm-notification-meta {
display: inline-flex;
flex-wrap: wrap;
gap: 0.75rem;
font-size: 0.85rem;
color: #64748b;
}
.tm-notification-meta i {
margin-right: 0.3rem;
color: #94a3b8;
}
.tm-notification-card-actions {
display: inline-flex;
align-items: center;
gap: 0.75rem;
}
.tm-notification-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.15rem 0.7rem;
border-radius: 999px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.tm-notification-pill.is-read {
background: rgba(148, 163, 184, 0.25);
color: #475569;
}
.tm-notification-pill.is-unread {
background: rgba(248, 113, 113, 0.25);
color: #b91c1c;
}
/*.tm-notification-view {*/
/* margin-top: 1.5rem;*/
/*}*/
.tm-notification-view-card {
background: #fff;
border: 1px solid rgba(148, 163, 184, 0.25);
border-radius: 1rem;
box-shadow: 0 30px 60px rgba(15, 23, 42, 0.12);
padding: 1.75rem;
margin-top: 1.5rem;
}
.tm-notification-view-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.tm-notification-view-label {
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.75rem;
color: #94a3b8;
margin: 0 0 0.35rem;
}
.tm-notification-view-title {
margin: 0;
color: #0f172a;
}
.tm-notification-view-body {
font-size: 0.95rem;
line-height: 1.7;
color: #1e293b;
white-space: pre-wrap;
}
.tm-notification-view-body p {
margin-bottom: 1rem;
}
.tm-empty-state,
.tm-notification-empty {
padding: 3rem 1.5rem;
text-align: center;
border: 2px dashed rgba(148, 163, 184, 0.45);
border-radius: 1.25rem;
background: rgba(248, 250, 252, 0.9);
/* margin: 1.5rem auto 0;*/
margin: 0 auto 0;
max-width: 640px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
}
.tm-empty-state > i,
.tm-notification-empty > i {
font-size: 3rem;
color: #94a3b8;
margin-bottom: 0.9rem;
}
.tm-empty-state h3,
.tm-notification-empty h3 {
margin-bottom: 0.5rem;
font-size: 1.5rem;
color: #0f172a;
font-weight: 600;
}
.tm-empty-state p,
.tm-notification-empty p {
margin: 0 auto;
max-width: 460px;
color: #475569;
line-height: 1.6;
}
.tm-empty-state-actions {
margin-top: 1.25rem;
display: inline-flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
}
.tm-directory-tags { .tm-directory-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1176,6 +1757,7 @@ html, body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem;
} }
.tm-call-card-actions .btn { .tm-call-card-actions .btn {
@ -1183,6 +1765,17 @@ html, body {
font-size: 0.85rem; font-size: 0.85rem;
} }
@media (max-width: 1200px) {
.tm-call-card {
grid-template-columns: minmax(220px, 1fr) minmax(0, 1fr);
}
.tm-call-card-actions {
grid-column: 1 / -1;
justify-content: flex-start;
}
}
@media (max-width: 992px) { @media (max-width: 992px) {
.tm-call-card { .tm-call-card {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -1656,7 +2249,7 @@ html, body {
.dashboard-stats-row { .dashboard-stats-row {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
justify-content: center; justify-content: flex-start;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {