diff --git a/app/helpers/theme.php b/app/helpers/theme.php index 81a7f8e..55a420a 100644 --- a/app/helpers/theme.php +++ b/app/helpers/theme.php @@ -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 * diff --git a/app/pages/theme.php b/app/pages/theme.php index 7c35794..4b741d1 100644 --- a/app/pages/theme.php +++ b/app/pages/theme.php @@ -51,11 +51,20 @@ if (isset($_GET['switch_to'])) { $themes = \App\Helpers\Theme::getAvailableThemes(); $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 = []; foreach ($themes as $id => $name) { + $meta = \App\Helpers\Theme::getThemeMetadata($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'), 'isActive' => $id === $currentTheme ]; diff --git a/app/templates/theme.php b/app/templates/theme.php index 068f584..39825d2 100644 --- a/app/templates/theme.php +++ b/app/templates/theme.php @@ -10,35 +10,124 @@ * - isActive: Whether this is the current theme */ ?> -
-

Theme switcher

-

Select a theme to change the appearance of the application.

-
- $theme): ?> -
-
- -
- -
No preview available
- -
- -
Current theme
- -
-
-

Theme ID:

-
- - Switch to this theme - - - -
-
-
-
- -
+ + +
+
+
+
+
+

Themes

+

Personalize with custom visual styles.

+
+ + + available + + + + Active: + +
+
+
+
+ + +
diff --git a/public_html/static/css/main.css b/public_html/static/css/main.css index 6575b13..ff15454 100644 --- a/public_html/static/css/main.css +++ b/public_html/static/css/main.css @@ -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 { width: 100%; min-height: 100%; @@ -86,6 +293,46 @@ html, body { 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 { display: inline-flex; align-items: center; @@ -504,18 +751,40 @@ html, body { /* Dashboard widgets */ .tm-widget-card { 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); - padding: 1.75rem; +/* padding: 1.75rem;*/ margin-bottom: 1.75rem; + overflow: hidden; } .tm-widget-header { display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 1rem; - margin-bottom: 1rem; + align-items: center; +/* gap: 0.85rem;*/ + background: linear-gradient(135deg, #eef2ff 0%, #e0f2fe 100%); +/* 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 { @@ -531,13 +800,18 @@ html, body { border: none; padding: 0; margin: 0; - font-size: 1.35rem; +/* font-size: 1.35rem;*/ + font-size: 1rem; font-weight: 600; color: #0f172a; - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; + display: inline-flex; + align-items: baseline; +/* gap: 0.4rem;*/ + cursor: default; +} + +.tm-widget-title a { + text-decoration: none; } .tm-widget-title:focus { @@ -583,7 +857,9 @@ html, body { .tm-widget-body { 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 { @@ -848,6 +1124,7 @@ html, body { border: 1px solid rgba(148, 163, 184, 0.5); border-radius: 0.85rem; padding: 0.55rem 0.85rem; + height: 100%; } .tm-filter-input input { @@ -872,9 +1149,51 @@ html, body { gap: 1rem; flex-wrap: nowrap; width: 100%; + height: 2.25rem; /* 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 { margin-bottom: 0; } @@ -884,6 +1203,11 @@ html, body { 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) { .tm-call-filter-row { flex-wrap: wrap; @@ -902,7 +1226,8 @@ html, body { display: inline-flex; align-items: center; padding: 0.2rem 0.6rem; - border-radius: 999px; +/* border-radius: 999px;*/ + border-radius: 10px; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; @@ -913,18 +1238,52 @@ html, body { .tm-directory-filter-form .form-select, .tm-directory-filter-form select { width: 100%; + background: transparent; + border: none; } .tm-directory-filter-form .select2-container { 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 { display: flex; gap: 0.75rem; flex-wrap: wrap; } + + .tm-directory-meta { display: flex; align-items: center; @@ -1036,6 +1395,7 @@ html, body { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.3rem 0.65rem; align-items: center; + position: relative; } .tm-directory-field { @@ -1057,6 +1417,227 @@ html, body { 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 { display: flex; flex-wrap: wrap; @@ -1176,6 +1757,7 @@ html, body { display: flex; align-items: center; justify-content: flex-end; + gap: 0.5rem; } .tm-call-card-actions .btn { @@ -1183,6 +1765,17 @@ html, body { 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) { .tm-call-card { grid-template-columns: 1fr; @@ -1656,7 +2249,7 @@ html, body { .dashboard-stats-row { margin-bottom: 1.5rem; - justify-content: center; + justify-content: flex-start; } @media (min-width: 1200px) {