mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update theme components
This commit is contained in:
@@ -389,6 +389,8 @@ APP_SERVICE=laravel.test
|
||||
# Port mapping: host → container
|
||||
APP_PORT=80
|
||||
VITE_PORT=5173
|
||||
# Browser-reachable host for Docker Vite hot reload URLs and HMR.
|
||||
VITE_DEV_SERVER_HOST=localhost
|
||||
FORWARD_DB_PORT=3306
|
||||
FORWARD_REDIS_PORT=6379
|
||||
FORWARD_MAILPIT_PORT=1025
|
||||
|
||||
@@ -41,6 +41,12 @@ class ContentSecurityPolicy
|
||||
|
||||
// Reuse the nonce generated above (shared with Blade via csp_nonce())
|
||||
|
||||
$viteDevServerSources = $this->viteDevServerSources();
|
||||
$viteAssetSource = $viteDevServerSources === null ? '' : ' '.$viteDevServerSources['asset'];
|
||||
$viteConnectSources = $viteDevServerSources === null
|
||||
? ''
|
||||
: ' '.$viteDevServerSources['asset'].' '.$viteDevServerSources['websocket'];
|
||||
|
||||
// Build CSP directives for non-Turnstile pages
|
||||
// 'strict-dynamic' propagates trust from nonce-validated scripts to
|
||||
// dynamically loaded scripts (e.g. TinyMCE loaded via createElement).
|
||||
@@ -51,14 +57,14 @@ class ContentSecurityPolicy
|
||||
// standard Alpine evaluator (new Function) that cannot be tree-shaken, and
|
||||
// TinyMCE (loaded from CDN on admin content pages) also relies on eval.
|
||||
// 'unsafe-inline' has been removed in favor of nonce-based validation.
|
||||
"script-src 'self' 'nonce-{$nonce}' 'unsafe-eval' 'strict-dynamic' https://challenges.cloudflare.com https://cdn.tiny.cloud https://cdn.jsdelivr.net/ https://static.cloudflareinsights.com/ https://cdnjs.cloudflare.com/ https://unpkg.com/ https://cdn.tailwindcss.com/ https://code.jquery.com https://apis.google.com https://www.google.com https://www.gstatic.com https://ajax.cloudflare.com blob:",
|
||||
"script-src-elem 'self' 'nonce-{$nonce}' https://challenges.cloudflare.com https://cdn.tiny.cloud https://cdn.jsdelivr.net/ https://static.cloudflareinsights.com/ https://cdnjs.cloudflare.com/ https://unpkg.com/ https://cdn.tailwindcss.com/ https://code.jquery.com https://apis.google.com https://www.google.com https://www.gstatic.com https://ajax.cloudflare.com",
|
||||
"script-src 'self' 'nonce-{$nonce}' 'unsafe-eval' 'strict-dynamic' https://challenges.cloudflare.com https://cdn.tiny.cloud https://cdn.jsdelivr.net/ https://static.cloudflareinsights.com/ https://cdnjs.cloudflare.com/ https://unpkg.com/ https://cdn.tailwindcss.com/ https://code.jquery.com https://apis.google.com https://www.google.com https://www.gstatic.com https://ajax.cloudflare.com blob:{$viteAssetSource}",
|
||||
"script-src-elem 'self' 'nonce-{$nonce}' https://challenges.cloudflare.com https://cdn.tiny.cloud https://cdn.jsdelivr.net/ https://static.cloudflareinsights.com/ https://cdnjs.cloudflare.com/ https://unpkg.com/ https://cdn.tailwindcss.com/ https://code.jquery.com https://apis.google.com https://www.google.com https://www.gstatic.com https://ajax.cloudflare.com{$viteAssetSource}",
|
||||
"script-src-attr 'none'",
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.bunny.net https://cdn.jsdelivr.net/ https://cdnjs.cloudflare.com/ https://cdn.tiny.cloud",
|
||||
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.bunny.net https://cdn.jsdelivr.net/ https://cdnjs.cloudflare.com/ https://cdn.tiny.cloud",
|
||||
"font-src 'self' https://fonts.gstatic.com https://fonts.bunny.net https://cdnjs.cloudflare.com/ https://cdn.tiny.cloud data:",
|
||||
"img-src 'self' data: https: blob:",
|
||||
"connect-src 'self' https://www.google.com https://cdn.tiny.cloud https://sp.tinymce.com",
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.bunny.net https://cdn.jsdelivr.net/ https://cdnjs.cloudflare.com/ https://cdn.tiny.cloud{$viteAssetSource}",
|
||||
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.bunny.net https://cdn.jsdelivr.net/ https://cdnjs.cloudflare.com/ https://cdn.tiny.cloud{$viteAssetSource}",
|
||||
"font-src 'self' https://fonts.gstatic.com https://fonts.bunny.net https://cdnjs.cloudflare.com/ https://cdn.tiny.cloud data:{$viteAssetSource}",
|
||||
"img-src 'self' data: https: blob:{$viteAssetSource}",
|
||||
"connect-src 'self' https://www.google.com https://cdn.tiny.cloud https://sp.tinymce.com{$viteConnectSources}",
|
||||
"frame-src 'self' https://www.google.com https://www.gstatic.com https://challenges.cloudflare.com https://cdn.tiny.cloud data: blob:",
|
||||
"child-src 'self' https://www.google.com https://challenges.cloudflare.com https://cdn.tiny.cloud blob:",
|
||||
"worker-src 'self' blob:",
|
||||
@@ -83,4 +89,40 @@ class ContentSecurityPolicy
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{asset: string, websocket: string}|null
|
||||
*/
|
||||
private function viteDevServerSources(): ?array
|
||||
{
|
||||
if (config('app.env') !== 'local' || ! Vite::isRunningHot()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hotUrl = trim((string) file_get_contents(Vite::hotFile()));
|
||||
$parts = parse_url($hotUrl);
|
||||
|
||||
if (! is_array($parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$scheme = $parts['scheme'] ?? null;
|
||||
$host = $parts['host'] ?? null;
|
||||
|
||||
if (! in_array($scheme, ['http', 'https'], true)
|
||||
|| ! is_string($host)
|
||||
|| ! in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$urlHost = str_contains($host, ':') ? "[{$host}]" : $host;
|
||||
$port = isset($parts['port']) ? ':'.$parts['port'] : '';
|
||||
$assetOrigin = "{$scheme}://{$urlHost}{$port}";
|
||||
$websocketScheme = $scheme === 'https' ? 'wss' : 'ws';
|
||||
|
||||
return [
|
||||
'asset' => $assetOrigin,
|
||||
'websocket' => "{$websocketScheme}://{$urlHost}{$port}",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ listen.group = www-data
|
||||
listen.mode = 0660
|
||||
EOF
|
||||
|
||||
# Vite is started separately with `make npm-dev`. A hot file left behind by a
|
||||
# previous dev-server process makes Laravel request assets from a dead port.
|
||||
unlink /var/www/html/public/hot 2>/dev/null || true
|
||||
|
||||
# Set timezone
|
||||
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
|
||||
@@ -152,6 +152,249 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Shared application shell */
|
||||
.skip-link {
|
||||
@apply fixed left-4 top-3 z-[100] -translate-y-20 rounded-xl bg-white px-4 py-2 text-sm font-semibold text-gray-950 shadow-xl transition-transform focus:translate-y-0 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
:where(a, button, input, select, textarea, summary):focus-visible {
|
||||
outline: 2px solid var(--color-primary-400);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.layout-shell {
|
||||
background-color: var(--surface-body);
|
||||
}
|
||||
|
||||
.dark .layout-shell {
|
||||
background-color: var(--surface-body-dark);
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
background-color: var(--surface-body);
|
||||
background-image:
|
||||
radial-gradient(circle at 92% 0%, color-mix(in srgb, var(--color-primary-500) 9%, transparent), transparent 28rem),
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--surface-body) 92%, white), var(--surface-body));
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.dark .layout-main {
|
||||
background-color: var(--surface-body-dark);
|
||||
background-image:
|
||||
radial-gradient(circle at 92% 0%, color-mix(in srgb, var(--color-primary-500) 13%, transparent), transparent 30rem),
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--surface-body-dark) 88%, black), var(--surface-body-dark));
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
border-right: 1px solid var(--surface-chrome-border);
|
||||
background-image:
|
||||
radial-gradient(circle at 10% 0%, color-mix(in srgb, var(--color-primary-400) 18%, transparent), transparent 18rem),
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--surface-sidebar) 92%, black), var(--surface-sidebar));
|
||||
box-shadow: 12px 0 36px rgb(15 23 42 / 0.14);
|
||||
}
|
||||
|
||||
.dark .layout-sidebar {
|
||||
border-right-color: var(--surface-chrome-border-dark);
|
||||
background-image:
|
||||
radial-gradient(circle at 10% 0%, color-mix(in srgb, var(--color-primary-500) 15%, transparent), transparent 18rem),
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--surface-sidebar-dark) 92%, black), var(--surface-sidebar-dark));
|
||||
box-shadow: 12px 0 40px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
.layout-sidebar__brand {
|
||||
min-height: 4.75rem;
|
||||
border-bottom: 1px solid var(--surface-chrome-border);
|
||||
}
|
||||
|
||||
.dark .layout-sidebar__brand {
|
||||
border-bottom-color: var(--surface-chrome-border-dark);
|
||||
}
|
||||
|
||||
.layout-sidebar__logo {
|
||||
border: 1px solid rgb(255 255 255 / 0.1);
|
||||
background: rgb(255 255 255 / 0.07);
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.08);
|
||||
}
|
||||
|
||||
.layout-sidebar__nav {
|
||||
scrollbar-color: rgb(255 255 255 / 0.18) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.user-sidebar-menu > .sidebar-section > button,
|
||||
.user-sidebar-menu > a,
|
||||
.admin-sidebar-menu > div > button,
|
||||
.admin-sidebar-menu > div > a {
|
||||
min-height: 2.75rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.user-sidebar-menu .sidebar-submenu,
|
||||
.admin-sidebar-menu [x-show] {
|
||||
border-left: 1px solid rgb(255 255 255 / 0.09);
|
||||
}
|
||||
|
||||
.user-sidebar-menu .sidebar-submenu a,
|
||||
.admin-sidebar-menu [x-show] a {
|
||||
min-height: 2.5rem;
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
|
||||
.admin-sidebar-menu > div {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.layout-topbar {
|
||||
border-bottom: 1px solid var(--surface-chrome-border);
|
||||
background-image: linear-gradient(to right, color-mix(in srgb, var(--surface-header) 94%, black), var(--surface-header));
|
||||
box-shadow: 0 8px 30px rgb(15 23 42 / 0.12);
|
||||
}
|
||||
|
||||
.dark .layout-topbar {
|
||||
border-bottom-color: var(--surface-chrome-border-dark);
|
||||
background-image: linear-gradient(to right, color-mix(in srgb, var(--surface-header-dark) 94%, black), var(--surface-header-dark));
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 0.25);
|
||||
}
|
||||
|
||||
.layout-primary-nav__search {
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 0 rgb(255 255 255 / 0.08);
|
||||
}
|
||||
|
||||
.layout-flash {
|
||||
@apply flex items-start gap-3 rounded-2xl border px-4 py-3.5 text-sm font-medium shadow-sm;
|
||||
}
|
||||
|
||||
.layout-flash > i {
|
||||
@apply mt-0.5 shrink-0 text-base;
|
||||
}
|
||||
|
||||
.layout-flash--success {
|
||||
@apply border-emerald-200 bg-emerald-50/95 text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/80 dark:text-emerald-100;
|
||||
}
|
||||
|
||||
.layout-flash--error {
|
||||
@apply border-red-200 bg-red-50/95 text-red-900 dark:border-red-800 dark:bg-red-950/80 dark:text-red-100;
|
||||
}
|
||||
|
||||
.layout-flash--warning {
|
||||
@apply border-amber-200 bg-amber-50/95 text-amber-950 dark:border-amber-800 dark:bg-amber-950/80 dark:text-amber-100;
|
||||
}
|
||||
|
||||
.app-page-header,
|
||||
.admin-page-header {
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
background-image: linear-gradient(120deg, color-mix(in srgb, var(--color-primary-50) 72%, white), transparent 62%);
|
||||
}
|
||||
|
||||
.dark .app-page-header,
|
||||
.dark .admin-page-header {
|
||||
border-bottom-color: var(--border-default-dark);
|
||||
background-image: linear-gradient(120deg, color-mix(in srgb, var(--color-primary-950) 35%, transparent), transparent 62%);
|
||||
}
|
||||
|
||||
.admin-page-header__icon {
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary-500) 24%, transparent);
|
||||
background-color: color-mix(in srgb, var(--color-primary-500) 11%, transparent);
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
|
||||
.dark .admin-page-header__icon {
|
||||
color: var(--color-primary-300);
|
||||
}
|
||||
|
||||
.admin-card,
|
||||
.admin-stat-card,
|
||||
.admin-data-table-wrap {
|
||||
border-color: var(--border-default);
|
||||
}
|
||||
|
||||
.dark .admin-card,
|
||||
.dark .admin-stat-card,
|
||||
.dark .admin-data-table-wrap {
|
||||
border-color: var(--border-default-dark);
|
||||
}
|
||||
|
||||
.admin-stat-card {
|
||||
background-image: linear-gradient(145deg, color-mix(in srgb, var(--color-primary-50) 42%, transparent), transparent 58%);
|
||||
}
|
||||
|
||||
.dark .admin-stat-card {
|
||||
background-image: linear-gradient(145deg, color-mix(in srgb, var(--color-primary-950) 24%, transparent), transparent 58%);
|
||||
}
|
||||
|
||||
.admin-data-table-wrap {
|
||||
border-radius: 0.875rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-page__hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-image:
|
||||
radial-gradient(circle at 92% 20%, color-mix(in srgb, var(--color-primary-400) 18%, transparent), transparent 16rem),
|
||||
linear-gradient(125deg, color-mix(in srgb, var(--color-primary-50) 78%, white), var(--surface-card) 62%);
|
||||
}
|
||||
|
||||
.dark .admin-dashboard-page__hero {
|
||||
background-image:
|
||||
radial-gradient(circle at 92% 20%, color-mix(in srgb, var(--color-primary-500) 16%, transparent), transparent 18rem),
|
||||
linear-gradient(125deg, color-mix(in srgb, var(--color-primary-950) 38%, var(--surface-card-dark)), var(--surface-card-dark) 62%);
|
||||
}
|
||||
|
||||
.admin-dashboard-page__stats-grid > div,
|
||||
.admin-dashboard-page [data-widget] {
|
||||
border-color: var(--border-default);
|
||||
border-radius: 1rem;
|
||||
background-color: var(--surface-card) !important;
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.04), 0 10px 30px rgb(15 23 42 / 0.04);
|
||||
}
|
||||
|
||||
.dark .admin-dashboard-page__stats-grid > div,
|
||||
.dark .admin-dashboard-page [data-widget] {
|
||||
border-color: var(--border-default-dark);
|
||||
background-color: var(--surface-card-dark) !important;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.18), 0 12px 32px rgb(0 0 0 / 0.12);
|
||||
}
|
||||
|
||||
.admin-dashboard-page__stats-grid > div {
|
||||
background-image: linear-gradient(145deg, color-mix(in srgb, var(--color-primary-50) 36%, transparent), transparent 58%);
|
||||
}
|
||||
|
||||
.dark .admin-dashboard-page__stats-grid > div {
|
||||
background-image: linear-gradient(145deg, color-mix(in srgb, var(--color-primary-950) 20%, transparent), transparent 58%);
|
||||
}
|
||||
|
||||
#sidebar a[aria-current="page"] {
|
||||
background-color: color-mix(in srgb, var(--color-primary-500) 22%, transparent);
|
||||
color: white;
|
||||
box-shadow: inset 3px 0 0 var(--color-primary-400);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.layout-sidebar {
|
||||
box-shadow: 20px 0 60px rgb(2 6 23 / 0.42);
|
||||
}
|
||||
|
||||
.layout-topbar__tools {
|
||||
max-width: 60vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Default document flow - public/guest pages need vertical scrolling */
|
||||
html,
|
||||
body {
|
||||
|
||||
@@ -6,9 +6,19 @@ import Alpine from '@alpinejs/csp';
|
||||
Alpine.data('adminSubmenu', () => ({
|
||||
open: false,
|
||||
|
||||
init() {
|
||||
this.open = Array.from(this.$el.querySelectorAll('a[href]')).some((link) => {
|
||||
try {
|
||||
var linkUrl = new URL(link.href, window.location.origin);
|
||||
return linkUrl.origin === window.location.origin && linkUrl.pathname === window.location.pathname;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,17 @@ Alpine.data('mobileSidebar', () => ({
|
||||
* Document-level delegation for mobile menu elements without x-data.
|
||||
*/
|
||||
(function() {
|
||||
document.querySelectorAll('#sidebar a[href]').forEach(function(link) {
|
||||
try {
|
||||
var linkUrl = new URL(link.href, window.location.origin);
|
||||
if (linkUrl.origin === window.location.origin && linkUrl.pathname === window.location.pathname) {
|
||||
link.setAttribute('aria-current', 'page');
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore malformed or non-navigational URLs.
|
||||
}
|
||||
});
|
||||
|
||||
var toggle = document.getElementById('mobile-menu-toggle');
|
||||
var panel = document.getElementById('mobile-nav-panel');
|
||||
var iconOpen = document.getElementById('mobile-menu-icon-open');
|
||||
@@ -82,6 +93,7 @@ Alpine.data('mobileSidebar', () => ({
|
||||
searchToggle.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
searchForm.classList.toggle('hidden');
|
||||
searchToggle.setAttribute('aria-expanded', searchForm.classList.contains('hidden') ? 'false' : 'true');
|
||||
if (panel && !searchForm.classList.contains('hidden')) {
|
||||
panel.classList.add('hidden');
|
||||
if (iconOpen && iconClose) { iconOpen.classList.remove('hidden'); iconClose.classList.add('hidden'); }
|
||||
@@ -91,10 +103,44 @@ Alpine.data('mobileSidebar', () => ({
|
||||
}
|
||||
|
||||
var mobileSidebarToggle = document.getElementById('mobile-sidebar-toggle');
|
||||
var mobileSidebarClose = document.getElementById('mobile-sidebar-close');
|
||||
var mobileSidebarBackdrop = document.getElementById('mobile-sidebar-backdrop');
|
||||
|
||||
function setMobileSidebar(open) {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar || !mobileSidebarToggle) return;
|
||||
|
||||
sidebar.classList.toggle('hidden', !open);
|
||||
sidebar.classList.toggle('flex', open);
|
||||
mobileSidebarBackdrop?.classList.toggle('hidden', !open);
|
||||
mobileSidebarToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
mobileSidebarToggle.setAttribute('aria-label', open ? 'Close navigation' : 'Open navigation');
|
||||
|
||||
if (open) {
|
||||
mobileSidebarClose?.focus();
|
||||
} else if (window.innerWidth < 768) {
|
||||
mobileSidebarToggle.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (mobileSidebarToggle && !mobileSidebarToggle.closest('[x-data]')) {
|
||||
mobileSidebarToggle.addEventListener('click', function() {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) { sidebar.classList.toggle('hidden'); sidebar.classList.toggle('flex'); }
|
||||
if (sidebar) setMobileSidebar(sidebar.classList.contains('hidden'));
|
||||
});
|
||||
|
||||
mobileSidebarClose?.addEventListener('click', function() {
|
||||
setMobileSidebar(false);
|
||||
});
|
||||
|
||||
mobileSidebarBackdrop?.addEventListener('click', function() {
|
||||
setMobileSidebar(false);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape' && mobileSidebarToggle.getAttribute('aria-expanded') === 'true') {
|
||||
setMobileSidebar(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,5 +151,9 @@ Alpine.data('mobileSidebar', () => ({
|
||||
if (iconOpen && iconClose) { iconOpen.classList.remove('hidden'); iconClose.classList.add('hidden'); }
|
||||
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
if (window.innerWidth >= 768 && mobileSidebarToggle) {
|
||||
setMobileSidebar(false);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -14,15 +14,17 @@
|
||||
|
||||
<div x-data="adminDashboard"
|
||||
id="adminDashboard"
|
||||
class="admin-dashboard-page"
|
||||
data-data-url="{{ route('admin.api.dashboard-data') }}"
|
||||
data-refresh-interval="{{ 60 * 1000 }}">
|
||||
<div class="space-y-6" data-dashboard-content>
|
||||
<div class="admin-dashboard-page__content space-y-6" data-dashboard-content>
|
||||
<!-- Welcome Section -->
|
||||
<x-admin.card class="p-6">
|
||||
<x-admin.card class="admin-dashboard-page__hero p-6">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-2">Admin Dashboard</h2>
|
||||
<p class="text-gray-600">Welcome to the administration panel</p>
|
||||
<p class="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-primary-600 dark:text-primary-300">Operations overview</p>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-gray-950 dark:text-white sm:text-3xl">Admin Dashboard</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-300">Monitor index health, users, releases, and site activity from one workspace.</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-50 dark:bg-gray-900 px-4 py-3 text-sm text-gray-600 dark:text-gray-300 lg:text-right">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-200">
|
||||
@@ -34,7 +36,7 @@
|
||||
</x-admin.card>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6">
|
||||
<div class="admin-dashboard-page__stats-grid grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<!-- Total Releases -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
'noPadding' => false,
|
||||
])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700']) }}>
|
||||
<div {{ $attributes->merge(['class' => 'admin-card surface-panel rounded-2xl border shadow-sm']) }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'striped' => false,
|
||||
])
|
||||
|
||||
<div class="overflow-x-auto overscroll-x-contain">
|
||||
<div class="admin-data-table-wrap overflow-x-auto overscroll-x-contain">
|
||||
<table {{ $attributes->merge(['class' => 'min-w-full table-auto divide-y divide-gray-200 dark:divide-gray-700']) }}>
|
||||
@if(isset($head))
|
||||
<thead @class([
|
||||
|
||||
@@ -5,24 +5,25 @@
|
||||
'subtitle' => null,
|
||||
])
|
||||
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-wrap justify-between items-center gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div {{ $attributes->merge(['class' => 'admin-page-header px-4 py-5 sm:px-6']) }}>
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<h1 class="flex items-center gap-3 text-2xl font-semibold tracking-tight text-gray-950 dark:text-white">
|
||||
@if($icon)
|
||||
<i class="{{ $icon }} mr-2"></i>
|
||||
<span class="admin-page-header__icon flex h-10 w-10 shrink-0 items-center justify-center rounded-xl" aria-hidden="true">
|
||||
<i class="{{ $icon }}"></i>
|
||||
</span>
|
||||
@endif
|
||||
{{ $title }}
|
||||
<span class="break-words">{{ $title }}</span>
|
||||
</h1>
|
||||
@if($subtitle)
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ $subtitle }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@if(isset($actions))
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="admin-page-header__actions flex flex-wrap gap-2">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
$colors = $colorMap[$color] ?? $colorMap['blue'];
|
||||
@endphp
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div {{ $attributes->merge(['class' => 'admin-stat-card surface-panel rounded-2xl border p-5 shadow-sm sm:p-6']) }}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">{{ $label }}</p>
|
||||
<p class="text-3xl font-bold text-gray-800 dark:text-gray-200">{{ $value }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 {{ $colors['bg'] }} rounded-lg flex items-center justify-center">
|
||||
<i class="{{ $icon }} text-2xl {{ $colors['text'] }}"></i>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl {{ $colors['bg'] }}">
|
||||
<i class="{{ $icon }} text-xl {{ $colors['text'] }}" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
@if($footer || isset($footerSlot))
|
||||
@@ -39,4 +39,3 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
'icon' => null,
|
||||
])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'px-6 py-6']) }}>
|
||||
<div {{ $attributes->merge(['class' => 'app-page-header px-4 py-5 sm:px-6 sm:py-6']) }}>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h1 class="flex items-center gap-3 text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl">
|
||||
<h1 class="flex items-center gap-3 text-2xl font-semibold tracking-tight text-gray-950 dark:text-white sm:text-3xl">
|
||||
@if($icon)
|
||||
<i class="{{ $icon }} text-primary-600 dark:text-primary-400" aria-hidden="true"></i>
|
||||
@endif
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
@isset($actions)
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<div class="app-page-header__actions flex shrink-0 flex-wrap items-center gap-2">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
@@ -27,31 +27,51 @@
|
||||
@stack('meta')
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="app-shell bg-gray-100 dark:bg-gray-900 font-sans antialiased">
|
||||
<div class="h-screen flex">
|
||||
<body class="app-shell font-sans antialiased">
|
||||
<a href="#main-content" class="skip-link">Skip to admin content</a>
|
||||
|
||||
<div class="layout-shell layout-shell--admin h-screen flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside id="sidebar" class="hidden md:flex md:flex-col w-64 bg-gray-900 dark:bg-gray-950 text-white shrink-0 h-full overflow-y-auto">
|
||||
<div class="flex items-center justify-between p-4 border-b border-white/10 dark:border-white/5">
|
||||
<a href="{{ route('admin.index') }}" class="flex items-center space-x-2">
|
||||
<i class="fas fa-cog text-2xl text-blue-500 dark:text-blue-400"></i>
|
||||
<span class="text-xl font-semibold">Admin Panel</span>
|
||||
<aside id="sidebar" class="layout-sidebar layout-sidebar--admin fixed inset-y-0 left-0 z-50 hidden h-full w-72 shrink-0 flex-col overflow-y-auto text-white md:static md:z-auto md:flex md:w-64" aria-label="Admin navigation">
|
||||
<div class="layout-sidebar__brand flex items-center justify-between gap-3 p-4">
|
||||
<a href="{{ route('admin.index') }}" class="flex min-w-0 items-center gap-3 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400">
|
||||
<span class="layout-sidebar__logo flex h-11 w-11 shrink-0 items-center justify-center rounded-xl">
|
||||
<i class="fas fa-sliders text-lg text-primary-300" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-lg font-semibold">Admin Panel</span>
|
||||
<span class="block text-xs font-medium text-white/55">Operations workspace</span>
|
||||
</span>
|
||||
</a>
|
||||
<button type="button" id="mobile-sidebar-close" class="touch-target inline-flex items-center justify-center rounded-xl text-white/70 hover:bg-white/10 hover:text-white md:hidden" aria-label="Close admin navigation">
|
||||
<i class="fas fa-xmark" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 overflow-y-auto py-4">
|
||||
<nav class="layout-sidebar__nav flex-1 overflow-y-auto py-3" aria-label="Admin">
|
||||
@include('partials.admin-menu')
|
||||
</nav>
|
||||
</aside>
|
||||
<button type="button" id="mobile-sidebar-backdrop" class="layout-sidebar-backdrop fixed inset-0 z-40 hidden bg-slate-950/60 backdrop-blur-sm md:hidden" aria-label="Close admin navigation" tabindex="-1"></button>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col h-full overflow-hidden">
|
||||
<div class="layout-content flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<!-- Top Bar -->
|
||||
<header class="surface-header bg-gray-800 dark:bg-gray-950 text-white shrink-0 z-10">
|
||||
<div class="flex items-center justify-between px-6 py-4">
|
||||
<h1 class="text-lg font-semibold text-gray-200">{{ $page_title ?? 'Admin Dashboard' }}</h1>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="theme-toggle" class="bg-gray-700 dark:bg-gray-800 text-gray-100 dark:text-gray-200 px-3 py-2 rounded-lg shadow hover:bg-gray-600 dark:hover:bg-gray-700 transition-all duration-200 flex items-center gap-2 touch-target"
|
||||
title="{{ ucfirst($userTheme) }} Mode">
|
||||
<header class="layout-topbar surface-header z-30 shrink-0 text-white">
|
||||
<div class="layout-topbar__inner flex min-h-16 items-center justify-between gap-3 px-3 py-2 sm:px-5 lg:px-6">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<button id="mobile-sidebar-toggle" type="button" class="touch-target inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/5 text-white/80 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 md:hidden" aria-label="Open admin navigation" aria-controls="sidebar" aria-expanded="false">
|
||||
<i class="fas fa-bars" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="min-w-0">
|
||||
<p class="text-[0.6875rem] font-semibold uppercase tracking-[0.16em] text-white/45">Administration</p>
|
||||
<p class="truncate text-sm font-semibold text-white sm:text-base">{{ $page_title ?? 'Dashboard' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-topbar__tools flex items-center gap-1 rounded-2xl border border-white/10 bg-white/5 p-1 shadow-sm backdrop-blur-sm sm:gap-2">
|
||||
<button id="theme-toggle" class="touch-target flex items-center gap-2 rounded-xl px-3 py-2 text-gray-100 transition hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400"
|
||||
title="{{ ucfirst($userTheme) }} Mode"
|
||||
aria-label="Change theme. Current theme: {{ $userTheme }}">
|
||||
<i id="theme-icon" class="fas
|
||||
@if($userTheme === 'dark')
|
||||
fa-moon
|
||||
@@ -65,13 +85,13 @@
|
||||
{{ ucfirst($userTheme) }}
|
||||
</span>
|
||||
</button>
|
||||
<a href="{{ url('/') }}" class="text-gray-300 dark:text-gray-400 hover:text-white transition">
|
||||
<i class="fas fa-home mr-1"></i> Back to Site
|
||||
<a href="{{ url('/') }}" class="touch-target inline-flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-white/75 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400" title="Back to site" aria-label="View site">
|
||||
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i><span class="hidden lg:inline">View site</span>
|
||||
</a>
|
||||
<a href="{{ route('logout') }}"
|
||||
data-logout
|
||||
class="text-red-400 hover:text-red-300 transition">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i> Logout
|
||||
class="touch-target inline-flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-red-300 transition hover:bg-red-400/10 hover:text-red-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-300" title="Log out" aria-label="Log out">
|
||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i><span class="hidden xl:inline">Log out</span>
|
||||
</a>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
|
||||
@csrf
|
||||
@@ -81,23 +101,23 @@
|
||||
</header>
|
||||
|
||||
<!-- Page Content - Scrollable Area -->
|
||||
<main class="flex-1 overflow-y-auto p-6 rounded-xl shadow-inner ring-1 ring-black/10 dark:ring-white/5" data-scroll-container>
|
||||
<main id="main-content" class="layout-main flex-1 overflow-y-auto p-3 sm:p-5 lg:p-6" data-scroll-container tabindex="-1">
|
||||
@unless(trim($__env->yieldContent('suppress_layout_flash')))
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-50 dark:bg-green-900/20 border-l-4 border-green-500 dark:border-green-600 text-green-800 dark:text-green-200 rounded">
|
||||
<i class="fas fa-check-circle mr-2"></i>{{ session('success') }}
|
||||
<div class="layout-flash layout-flash--success mb-5" role="status">
|
||||
<i class="fas fa-circle-check" aria-hidden="true"></i><div>{{ session('success') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 dark:border-red-600 text-red-800 dark:text-red-200 rounded">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i>{{ session('error') }}
|
||||
<div class="layout-flash layout-flash--error mb-5" role="alert">
|
||||
<i class="fas fa-circle-exclamation" aria-hidden="true"></i><div>{{ session('error') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('warning'))
|
||||
<div class="mb-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-500 dark:border-yellow-600 text-yellow-800 dark:text-yellow-200 rounded">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>{{ session('warning') }}
|
||||
<div class="layout-flash layout-flash--warning mb-5" role="status">
|
||||
<i class="fas fa-triangle-exclamation" aria-hidden="true"></i><div>{{ session('warning') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
@endunless
|
||||
@@ -106,7 +126,7 @@
|
||||
</main>
|
||||
|
||||
<!-- Admin Footer - Fixed at bottom -->
|
||||
<footer class="shrink-0">
|
||||
<footer class="layout-footer shrink-0">
|
||||
<div class="px-6 py-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 text-sm text-gray-300 dark:text-gray-400">
|
||||
<p>© {{ now()->year }} <a href="https://github.com/NNTmux/newznab-tmux" class="text-primary-400 hover:text-primary-300 transition">NNTmux</a> Admin Panel</p>
|
||||
@@ -146,4 +166,3 @@
|
||||
@endauth
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -33,43 +33,57 @@
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="app-shell font-sans antialiased">
|
||||
<div class="h-screen flex">
|
||||
<a href="#main-content" class="skip-link">Skip to content</a>
|
||||
|
||||
<div class="layout-shell h-screen flex">
|
||||
<!-- Sidebar -->
|
||||
@auth
|
||||
<aside id="sidebar" class="hidden md:flex md:flex-col w-64 text-white shrink-0 h-full overflow-y-auto">
|
||||
<div class="flex items-center justify-between p-4 border-b border-white/10 dark:border-white/5">
|
||||
<a href="{{ $site['home_link'] ?? url('/') }}" class="flex items-center space-x-3">
|
||||
<img src="{{ asset('assets/images/logo.svg') }}" alt="{{ config('app.name') }} Logo" class="w-12 h-12" aria-hidden="true">
|
||||
<span class="text-xl font-semibold">{{ config('app.name') }}</span>
|
||||
<aside id="sidebar" class="layout-sidebar fixed inset-y-0 left-0 z-50 hidden h-full w-72 shrink-0 flex-col overflow-y-auto text-white md:static md:z-auto md:flex md:w-64" aria-label="Account navigation">
|
||||
<div class="layout-sidebar__brand flex items-center justify-between gap-3 p-4">
|
||||
<a href="{{ $site['home_link'] ?? url('/') }}" class="flex min-w-0 items-center gap-3 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400">
|
||||
<span class="layout-sidebar__logo flex h-11 w-11 shrink-0 items-center justify-center rounded-xl">
|
||||
<i class="fas fa-layer-group text-lg text-primary-300" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-lg font-semibold">{{ config('app.name') }}</span>
|
||||
<span class="block text-xs font-medium text-white/55">Usenet workspace</span>
|
||||
</span>
|
||||
</a>
|
||||
<button type="button" id="mobile-sidebar-close" class="touch-target inline-flex items-center justify-center rounded-xl text-white/70 hover:bg-white/10 hover:text-white md:hidden" aria-label="Close navigation">
|
||||
<i class="fas fa-xmark" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 overflow-y-auto py-4">
|
||||
<nav class="layout-sidebar__nav flex-1 overflow-y-auto py-3" aria-label="User">
|
||||
@include('partials.sidebar')
|
||||
</nav>
|
||||
</aside>
|
||||
<button type="button" id="mobile-sidebar-backdrop" class="layout-sidebar-backdrop fixed inset-0 z-40 hidden bg-slate-950/60 backdrop-blur-sm md:hidden" aria-label="Close navigation" tabindex="-1"></button>
|
||||
@endauth
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col h-full overflow-hidden">
|
||||
<div class="layout-content flex h-full min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<!-- Top Navigation -->
|
||||
@auth
|
||||
<header class="surface-header text-white shrink-0 z-10">
|
||||
<header class="layout-topbar surface-header z-30 shrink-0 text-white">
|
||||
@include('partials.header-menu')
|
||||
</header>
|
||||
@endauth
|
||||
|
||||
<!-- Page Content - This is the scrollable area -->
|
||||
<main class="flex-1 overflow-y-auto shadow-inner ring-1 ring-black/10 dark:ring-white/5" data-scroll-container>
|
||||
<div class="container mx-auto px-4 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]">
|
||||
<main id="main-content" class="layout-main flex-1 overflow-y-auto" data-scroll-container tabindex="-1">
|
||||
<div class="layout-page-container container mx-auto max-w-[1600px] px-3 py-5 pb-[max(1.5rem,env(safe-area-inset-bottom))] sm:px-5 sm:py-6 lg:px-8">
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-100 dark:bg-green-900 border border-green-400 dark:border-green-700 text-green-700 dark:text-green-200 rounded-lg">
|
||||
{{ session('success') }}
|
||||
<div class="layout-flash layout-flash--success mb-5" role="status">
|
||||
<i class="fas fa-circle-check" aria-hidden="true"></i>
|
||||
<div>{{ session('success') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-200 rounded-lg">
|
||||
<div class="layout-flash layout-flash--error mb-5" role="alert">
|
||||
<i class="fas fa-circle-exclamation" aria-hidden="true"></i>
|
||||
<div>
|
||||
@if(is_array(session('error')))
|
||||
@foreach(session('error') as $error)
|
||||
<div>{{ $error }}</div>
|
||||
@@ -78,6 +92,7 @@
|
||||
{{ session('error') }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
@@ -95,8 +110,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Mobile Sidebar Toggle -->
|
||||
<button id="mobile-sidebar-toggle" class="md:hidden fixed z-50 bg-primary-600 dark:bg-primary-700 text-white p-4 rounded-full shadow-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition-all touch-target bottom-[max(5rem,calc(env(safe-area-inset-bottom)+4rem))] right-[max(1rem,env(safe-area-inset-right))]" aria-label="Toggle Sidebar">
|
||||
<i class="fas fa-bars text-lg"></i>
|
||||
<button id="mobile-sidebar-toggle" class="layout-mobile-sidebar-toggle touch-target fixed z-30 inline-flex items-center gap-2 rounded-full bg-primary-600 px-4 py-3 text-sm font-semibold text-white shadow-lg transition hover:bg-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-2 dark:bg-primary-700 dark:hover:bg-primary-600 md:hidden bottom-[max(5rem,calc(env(safe-area-inset-bottom)+4rem))] right-[max(1rem,env(safe-area-inset-right))]" aria-label="Open account navigation" aria-controls="sidebar" aria-expanded="false">
|
||||
<i class="fas fa-compass" aria-hidden="true"></i>
|
||||
<span>Navigate</span>
|
||||
</button>
|
||||
|
||||
<!-- Back to Top -->
|
||||
@@ -106,7 +122,8 @@
|
||||
@php $themePreference = $userTheme; @endphp
|
||||
@guest
|
||||
<button id="theme-toggle" class="fixed z-50 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-4 py-3 rounded-full shadow-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-all duration-200 flex items-center gap-2 touch-target bottom-[max(1rem,env(safe-area-inset-bottom))] right-[max(1rem,env(safe-area-inset-right))]"
|
||||
title="{{ ucfirst($themePreference) }} Mode">
|
||||
title="{{ ucfirst($themePreference) }} Mode"
|
||||
aria-label="Change theme. Current theme: {{ $themePreference }}">
|
||||
<i id="theme-icon" class="fas {{ $themePreference === 'dark' ? 'fa-moon' : ($themePreference === 'system' ? 'fa-desktop' : 'fa-sun') }}"></i>
|
||||
<span id="theme-label" class="text-xs font-medium hidden sm:inline">{{ ucfirst($themePreference) }}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="space-y-2 px-4">
|
||||
<div class="admin-sidebar-menu space-y-1 px-3 py-2">
|
||||
<!-- Dashboard -->
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.index') }}" class="flex items-center space-x-3 text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-white hover:bg-white/10 dark:hover:bg-white/5 py-2 px-3 rounded transition">
|
||||
@@ -330,4 +330,3 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<i class="fas mr-2" :class="iconClass()"></i>
|
||||
<span x-text="title">Confirm Action</span>
|
||||
</h3>
|
||||
<button type="button" @click="cancel()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
<button type="button" @click="cancel()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" aria-label="Close confirmation">
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,4 +50,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100" id="filelist-modal-title">
|
||||
<i class="fas fa-folder-open mr-2 text-blue-600 dark:text-blue-400"></i>File List
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()" aria-label="Close file list">
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,4 +60,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<nav class="bg-gray-800 dark:bg-gray-950 relative">
|
||||
<div class="container mx-auto px-4">
|
||||
<nav class="layout-primary-nav relative" aria-label="Content categories">
|
||||
<div class="container mx-auto max-w-[1600px] px-3 sm:px-5 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Mobile menu button (visible below lg) -->
|
||||
<button type="button" class="lg:hidden text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-white focus:outline-none p-2 touch-target" id="mobile-menu-toggle" aria-expanded="false" aria-controls="mobile-nav-panel">
|
||||
<i class="fas fa-bars text-xl" id="mobile-menu-icon-open"></i>
|
||||
<i class="fas fa-times text-xl hidden" id="mobile-menu-icon-close"></i>
|
||||
<button type="button" class="touch-target rounded-xl p-2 text-gray-300 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 lg:hidden" id="mobile-menu-toggle" aria-label="Toggle category navigation" aria-expanded="false" aria-controls="mobile-nav-panel">
|
||||
<i class="fas fa-bars text-xl" id="mobile-menu-icon-open" aria-hidden="true"></i>
|
||||
<i class="fas fa-times text-xl hidden" id="mobile-menu-icon-close" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<!-- Desktop Navigation (visible at lg+) -->
|
||||
<div class="hidden lg:flex lg:items-center lg:space-x-1 flex-1 min-w-0" id="desktop-nav">
|
||||
<div class="layout-primary-nav__categories hidden min-w-0 flex-1 lg:flex lg:items-center lg:gap-1" id="desktop-nav">
|
||||
@if(isset($parentcatlist))
|
||||
@foreach($parentcatlist as $parentcat)
|
||||
@if($parentcat['id'] == App\Models\Category::TV_ROOT)
|
||||
@@ -165,13 +165,14 @@
|
||||
<!-- Right side: Search and User Menu -->
|
||||
<div class="flex items-center space-x-2 lg:space-x-4 ml-auto">
|
||||
<!-- Mobile Search Toggle (visible below lg) -->
|
||||
<button type="button" class="lg:hidden text-gray-300 hover:text-white p-2 touch-target" id="mobile-search-toggle">
|
||||
<i class="fa fa-search text-lg"></i>
|
||||
<button type="button" class="touch-target rounded-xl p-2 text-gray-300 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 lg:hidden" id="mobile-search-toggle" aria-label="Toggle release search" aria-expanded="false" aria-controls="mobile-search-form">
|
||||
<i class="fa fa-search text-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<!-- Desktop Search Form with Autocomplete (visible at lg+) -->
|
||||
<form method="GET" action="{{ route('search') }}" class="hidden lg:flex items-center relative" id="header-search-form">
|
||||
<select name="t" class="bg-gray-700 text-white text-sm rounded-l-lg px-3 py-2 border-r border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<form method="GET" action="{{ route('search') }}" class="layout-primary-nav__search relative hidden items-center lg:flex" id="header-search-form" role="search">
|
||||
<label for="header-search-category" class="sr-only">Search category</label>
|
||||
<select id="header-search-category" name="t" class="bg-gray-700 text-white text-sm rounded-l-lg px-3 py-2 border-r border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<option value="-1">All</option>
|
||||
@if(isset($parentcatlist))
|
||||
@foreach($parentcatlist as $parentcat)
|
||||
@@ -183,19 +184,20 @@
|
||||
@endif
|
||||
</select>
|
||||
<div class="relative">
|
||||
<label for="header-search-input" class="sr-only">Search releases</label>
|
||||
<input type="search"
|
||||
name="search"
|
||||
id="header-search-input"
|
||||
value="{{ $header_menu_search ?? '' }}"
|
||||
placeholder="Search..."
|
||||
placeholder="Search releases"
|
||||
autocomplete="off"
|
||||
class="bg-gray-700 text-white text-sm px-3 py-2 w-40 xl:w-48 focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<!-- Autocomplete dropdown for header -->
|
||||
<div id="header-autocomplete-dropdown" class="hidden absolute z-50 w-64 mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto right-0">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="bg-primary-600 dark:bg-primary-700 hover:bg-primary-700 dark:hover:bg-primary-800 text-white px-4 py-2 rounded-r-lg transition">
|
||||
<i class="fa fa-search"></i>
|
||||
<button type="submit" class="bg-primary-600 dark:bg-primary-700 hover:bg-primary-700 dark:hover:bg-primary-800 text-white px-4 py-2 rounded-r-lg transition" aria-label="Search releases">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100" id="image-modal-title" x-text="imageTitle">
|
||||
Image Preview
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()" aria-label="Close image preview">
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -57,4 +57,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100" id="mediainfo-modal-title">
|
||||
<i class="fas fa-info-circle mr-2 text-blue-600 dark:text-blue-400"></i>Media Information
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()" aria-label="Close media information">
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,4 +60,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100" id="nfo-modal-title">
|
||||
<i class="fas fa-file-alt mr-2 text-yellow-600 dark:text-yellow-400"></i>NFO File
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()" aria-label="Close NFO preview">
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -67,4 +67,3 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100" id="preview-modal-title" x-text="title">
|
||||
Preview Image
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" @click="close()" aria-label="Close preview">
|
||||
<i class="fas fa-times text-xl" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -72,4 +72,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100" id="report-modal-title">
|
||||
<i class="fas fa-flag text-red-500 mr-2"></i>Report Release
|
||||
</h3>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" @click="close()">
|
||||
<i class="fas fa-times"></i>
|
||||
<button type="button" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" @click="close()" aria-label="Close report form">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav class="flex flex-col space-y-1 p-4">
|
||||
<div class="user-sidebar-menu flex flex-col gap-1 px-3 py-2">
|
||||
<!-- Browse Menu -->
|
||||
<div class="sidebar-section">
|
||||
<button class="sidebar-toggle w-full flex items-center justify-between px-4 py-3 text-white hover:bg-white/10 rounded-lg transition" data-target="submenu1">
|
||||
@@ -185,4 +185,4 @@
|
||||
@csrf
|
||||
</form>
|
||||
@endauth
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
<div class="flex-1 text-sm text-gray-700 dark:text-gray-300" x-text="toast.message"></div>
|
||||
<button type="button"
|
||||
@click="dismiss(toast.id)"
|
||||
class="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition">
|
||||
<i class="fas fa-times"></i>
|
||||
class="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition"
|
||||
aria-label="Dismiss notification">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
+62
-13
@@ -4,7 +4,8 @@
|
||||
"agents-md-generator": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "67546545840ed99772e5bfbf182cca3d6d6e9516311d0f5c070b2a0c66510879"
|
||||
"skillPath": "skills/agents-md-generator/SKILL.md",
|
||||
"computedHash": "57cdc578141bed1d8e4a62a0d1a4e0958c709772aea26a9792f2f5c4867b7434"
|
||||
},
|
||||
"angular-architect": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -14,6 +15,7 @@
|
||||
"api-design-patterns": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/api-design-patterns/SKILL.md",
|
||||
"computedHash": "3f15c74774287c26486439158e2f7454907b6751fbd2e7842f034555e47df796"
|
||||
},
|
||||
"api-designer": {
|
||||
@@ -36,6 +38,12 @@
|
||||
"sourceType": "github",
|
||||
"computedHash": "4513b7e0d937979a80b774ef9220b0074c5c4230ea5cbb39cca4f1c402529491"
|
||||
},
|
||||
"brainstorm-first": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brainstorm-first/SKILL.md",
|
||||
"computedHash": "abb9f785bf6c8b1c908903ed2a34a74211872d83e0a627ee9abcfa7f7be9f1f9"
|
||||
},
|
||||
"chaos-engineer": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
"sourceType": "github",
|
||||
@@ -44,6 +52,7 @@
|
||||
"clean-code-principles": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/clean-code-principles/SKILL.md",
|
||||
"computedHash": "4c22dca6110d58f1454523dc3f192ef20d91f538233da9132b407edd041a4762"
|
||||
},
|
||||
"cli-developer": {
|
||||
@@ -69,6 +78,7 @@
|
||||
"code-slop": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/code-slop/SKILL.md",
|
||||
"computedHash": "a4362f272da2f81751108d0bbfb8b5128e5067329619645d24b100c95dc3edd9"
|
||||
},
|
||||
"cpp-pro": {
|
||||
@@ -94,12 +104,14 @@
|
||||
"debugging-workflow": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "84e550fad0bb4b05c3dd8baa3babc09c9a2de05575d1380edc09a4dc53a763f8"
|
||||
"skillPath": "skills/debugging-workflow/SKILL.md",
|
||||
"computedHash": "fa82a9b28f17ba3a191938b1c1ce983a7efd6b2cf7db35dbb92b97994c003841"
|
||||
},
|
||||
"design-system-generator": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "3a3c396b7a0ada5c4b78db05bacdcb53d2bf3b5c6d3b7385b1973ffca8ffeef4"
|
||||
"skillPath": "skills/design-system-generator/SKILL.md",
|
||||
"computedHash": "33362adace6be7111912268ec53f0257f053a48c527255d010f6b4d4fab99463"
|
||||
},
|
||||
"devops-engineer": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -114,12 +126,14 @@
|
||||
"docker-local-dev": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "9af95f1ef718cda09bd0290f2966bea658d95d4d5a3ede763d083e7bc9af6bcb"
|
||||
"skillPath": "skills/docker-local-dev/SKILL.md",
|
||||
"computedHash": "48beea0c77d7dc0e7b36c8232ca4220b900f7d5e53d6e8bfacdb2ac6295f8694"
|
||||
},
|
||||
"documentation-guidelines": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "fa3e6b5d3f8abdcbbc35ddbb96dad0d789eb5f575778de44830da81627e9b351"
|
||||
"skillPath": "skills/documentation-guidelines/SKILL.md",
|
||||
"computedHash": "4062a2ceaad577c294182ba5c1bf8f25c3dfb092154f9caed98aec1da3310258"
|
||||
},
|
||||
"dotnet-core-expert": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -129,6 +143,7 @@
|
||||
"e2e-playwright-testing": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/e2e-playwright-testing/SKILL.md",
|
||||
"computedHash": "ee097dfeaa4d731a35ab47bd930859f17d7d0d073d0df3720435b02d97d12210"
|
||||
},
|
||||
"eloquent-best-practices": {
|
||||
@@ -179,6 +194,7 @@
|
||||
"git-workflow": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/git-workflow/SKILL.md",
|
||||
"computedHash": "e6a28779208771a57940a14e1f9e463ab65c1f8e660fcc52d05b9ec05de3fab1"
|
||||
},
|
||||
"golang-pro": {
|
||||
@@ -214,11 +230,19 @@
|
||||
"laravel-11-12-app-guidelines": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "296fdeb0605da6c0892f427cb7053fe6bd6514c1e748b81e2175f2f1a33db6c4"
|
||||
"skillPath": "skills/laravel-11-12-app-guidelines/SKILL.md",
|
||||
"computedHash": "98b9f9a9052b29dbae2a98aeb7b224b69572a227f3a9a2413816d94588df2c85"
|
||||
},
|
||||
"laravel-13-app-guidelines": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-13-app-guidelines/SKILL.md",
|
||||
"computedHash": "76bc2986af74c8cbadb77200edd0d485c6b0020ffda625e077aecd2965cff5cd"
|
||||
},
|
||||
"laravel-ai-sdk": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-ai-sdk/SKILL.md",
|
||||
"computedHash": "6e7fb4360bcbe6eea2a634d7b96a46b41887f60eca806f54b7400f3efa508ce0"
|
||||
},
|
||||
"laravel-api-resource-patterns": {
|
||||
@@ -234,6 +258,7 @@
|
||||
"laravel-best-practices": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-best-practices/SKILL.md",
|
||||
"computedHash": "6eb357774a2416b3e632cf4f7dec1ff9558af2d70c4b8ade802f5a65ce118fb5"
|
||||
},
|
||||
"laravel-blade-component-patterns": {
|
||||
@@ -254,6 +279,7 @@
|
||||
"laravel-database-optimization": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-database-optimization/SKILL.md",
|
||||
"computedHash": "564cbba91a281420e077178d7b367ef1c367576ff6f050f9ee6eaa99d30d2a1c"
|
||||
},
|
||||
"laravel-event-driven-architecture": {
|
||||
@@ -269,11 +295,13 @@
|
||||
"laravel-inertia-react": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-inertia-react/SKILL.md",
|
||||
"computedHash": "b05144f7f9df5fd3cd03ce13cba822ac61970449fdf7c4c4208684258e8b1724"
|
||||
},
|
||||
"laravel-mcp": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-mcp/SKILL.md",
|
||||
"computedHash": "31de735b826f5c8e7b64ca133b10f2fc6b4a1789bb6b144aecccc948fe8ef689"
|
||||
},
|
||||
"laravel-middleware-patterns": {
|
||||
@@ -289,6 +317,7 @@
|
||||
"laravel-owasp-security": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-owasp-security/SKILL.md",
|
||||
"computedHash": "35f74b7fb37c9b0cb1a274e7e25f7425e793dacd73620c3507c7e9462e4c9c9d"
|
||||
},
|
||||
"laravel-quality": {
|
||||
@@ -304,6 +333,7 @@
|
||||
"laravel-queues": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-queues/SKILL.md",
|
||||
"computedHash": "75391b4d5635e0db3ef8a02bd512a2974b98bae97375fdfd9a18fe7ad936dbf8"
|
||||
},
|
||||
"laravel-specialist": {
|
||||
@@ -329,6 +359,7 @@
|
||||
"laravel-testing": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/laravel-testing/SKILL.md",
|
||||
"computedHash": "9627c977341aa7a9a11e5f500838409c1e39e1ca0714c6a6038fba4d3a5203c9"
|
||||
},
|
||||
"laravel-validation-patterns": {
|
||||
@@ -379,7 +410,8 @@
|
||||
"office-web-ui-system": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "4bd0d7ff4fef14f07e0304226d671c8aa41cbd93923f0e3665e60096a819795d"
|
||||
"skillPath": "skills/office-web-ui-system/SKILL.md",
|
||||
"computedHash": "53d35e07b894b96a6cee99fd95145dd3464b453a80527812e5e997aeb9ee3cf2"
|
||||
},
|
||||
"pandas-pro": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -389,11 +421,13 @@
|
||||
"performance-optimization": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "41427c7ae99d43037755c878a1613ed2beb536d3464b56795197b75ff604419e"
|
||||
"skillPath": "skills/performance-optimization/SKILL.md",
|
||||
"computedHash": "1958e40d2e43cf06951f0ae703650ba474772308bd5d96fa05a3911c3264ede5"
|
||||
},
|
||||
"php-best-practices": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/php-best-practices/SKILL.md",
|
||||
"computedHash": "1b25a60572ddd8b206eaf68b067cde70a9d002df572de8fefda703532cecfdad"
|
||||
},
|
||||
"php-pro": {
|
||||
@@ -414,16 +448,19 @@
|
||||
"prd-writing": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/prd-writing/SKILL.md",
|
||||
"computedHash": "4c3141b2fdc77aac6a528bd9cee5e07d8122d75a89295f4020ceab112c1d44a9"
|
||||
},
|
||||
"project-development-mindset": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "a1386663bb117455b814ee0e7b41a7f2c450c149033a3eee48433be140e6c4c9"
|
||||
"skillPath": "skills/project-development-mindset/SKILL.md",
|
||||
"computedHash": "4666d6da044d29bfdb2560b6636715a1d0888750f00da8ca3186697897ee6734"
|
||||
},
|
||||
"project-docs": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/project-docs/SKILL.md",
|
||||
"computedHash": "d2da7474d61edf37883e91b31dec49efbcb5bc6979e8d132dceaa26c3fad1b61"
|
||||
},
|
||||
"prompt-engineer": {
|
||||
@@ -459,12 +496,14 @@
|
||||
"react-vite-best-practices": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/react-vite-best-practices/SKILL.md",
|
||||
"computedHash": "82994934665a490b227fabdfec3d148b1fa291d77d82ccf978e761b9e1ff6e4c"
|
||||
},
|
||||
"run-reviewable-subtask-loop": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "6e8a8a9c67d0331fdbb147adefc2798597d47a58f62877f1ba0ce33ec43478e9"
|
||||
"skillPath": "skills/run-reviewable-subtask-loop/SKILL.md",
|
||||
"computedHash": "11db9fa922b1da33704b9b8b2d2b22c1ca94a019dc66c78ac78bbbf16e0fbc2b"
|
||||
},
|
||||
"rust-engineer": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -489,6 +528,7 @@
|
||||
"seo-best-practices": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/seo-best-practices/SKILL.md",
|
||||
"computedHash": "4bc88f564290e835fd5f486bdacce1c3a867afaafa118363c9e80f6e6466a8d7"
|
||||
},
|
||||
"shopify-expert": {
|
||||
@@ -524,6 +564,7 @@
|
||||
"state-management": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/state-management/SKILL.md",
|
||||
"computedHash": "0bad07f74366ae633503a6f256eae7d7335c5ba9277110b4d788263d56a7a07a"
|
||||
},
|
||||
"swift-expert": {
|
||||
@@ -539,11 +580,13 @@
|
||||
"tailwind-best-practices": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/tailwind-best-practices/SKILL.md",
|
||||
"computedHash": "39145b4b9d12b154d9ff9261805d225557a1dcb5c881b1da8dcc027004aefd44"
|
||||
},
|
||||
"technical-debt": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/technical-debt/SKILL.md",
|
||||
"computedHash": "a127f457928fb6dc439751ec0cdb6edef14c78e90b6fd6e26ff31eb18b2e90c3"
|
||||
},
|
||||
"terraform-engineer": {
|
||||
@@ -559,12 +602,14 @@
|
||||
"testing-best-practices": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/testing-best-practices/SKILL.md",
|
||||
"computedHash": "590eb4064d9fa41854f7871991a3c1ef566553593338632c6cf88d9984194d61"
|
||||
},
|
||||
"testing-verification": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "fdf5289ec2a3659fb0c02658704223bf70640d149b10120929028e7d45e1b0fa"
|
||||
"skillPath": "skills/testing-verification/SKILL.md",
|
||||
"computedHash": "bc7d115e7c61895c4c4fb90c411323bc7ecad96b9487aa4ba2a731ae3cee6b4d"
|
||||
},
|
||||
"the-fool": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -579,6 +624,7 @@
|
||||
"typescript-react-patterns": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/typescript-react-patterns/SKILL.md",
|
||||
"computedHash": "650048e53310097d1e9029a668c0b39e5738ce70fbe3b23f35cd875fc05b48e5"
|
||||
},
|
||||
"ui-mockup-visualizer": {
|
||||
@@ -589,12 +635,14 @@
|
||||
"ui-ux-concept-implementation": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "c3931ae5e6f3c515588aac8c67fc3b793dfd0a7ba074ca562725734e121fe45a"
|
||||
"skillPath": "skills/ui-ux-concept-implementation/SKILL.md",
|
||||
"computedHash": "63af43791f6dd6f344f8df8be3e277c0e68d1c9ee4dc0e578a810476948451a5"
|
||||
},
|
||||
"vps-docker-traefik-deploy": {
|
||||
"source": "thienanblog/awesome-ai-agent-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "e73758b1fe424a7b8b502988d0ed2a0d455f7667e49c517789ce2438e185a61e"
|
||||
"skillPath": "skills/vps-docker-traefik-deploy/SKILL.md",
|
||||
"computedHash": "3bb0d7684a318cd04ac11b6c6091b57183c1b92396ef84fdd6ae660bff48ffd9"
|
||||
},
|
||||
"vue-expert": {
|
||||
"source": "jeffallan/claude-skills",
|
||||
@@ -609,6 +657,7 @@
|
||||
"web-design-guidelines": {
|
||||
"source": "asyrafhussin/agent-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/web-design-guidelines/SKILL.md",
|
||||
"computedHash": "e27800d4c6a4cf98c50776af70d0b0ea1ebf5b81a03992b2019309f0ea9e7012"
|
||||
},
|
||||
"websocket-engineer": {
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Tests\Unit;
|
||||
use App\Http\Middleware\ContentSecurityPolicy;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Vite;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
@@ -16,32 +17,48 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ContentSecurityPolicyTest extends TestCase
|
||||
{
|
||||
private ?Container $originalContainer;
|
||||
private Container $originalContainer;
|
||||
|
||||
private ?Application $originalFacadeApplication;
|
||||
|
||||
private string $hotFile;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->originalContainer = Container::getInstance();
|
||||
$facadeApplication = Facade::getFacadeApplication();
|
||||
$this->originalFacadeApplication = $facadeApplication instanceof Application ? $facadeApplication : null;
|
||||
|
||||
$container = new Container;
|
||||
$container->instance('config', new Repository([
|
||||
$application = new Application(dirname(__DIR__, 2));
|
||||
$application->instance('config', new Repository([
|
||||
'app' => ['env' => 'production'],
|
||||
'captcha' => [
|
||||
'provider' => null,
|
||||
'turnstile' => ['enabled' => false],
|
||||
],
|
||||
]));
|
||||
$container->instance(Vite::class, new Vite);
|
||||
$application->instance(Vite::class, new Vite);
|
||||
|
||||
Container::setInstance($container);
|
||||
Facade::setFacadeApplication($container);
|
||||
Container::setInstance($application);
|
||||
Facade::setFacadeApplication($application);
|
||||
Facade::clearResolvedInstances();
|
||||
|
||||
$this->hotFile = sys_get_temp_dir().'/nntmux-csp-hot-'.bin2hex(random_bytes(8));
|
||||
app(Vite::class)->useHotFile($this->hotFile);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (is_file($this->hotFile)) {
|
||||
unlink($this->hotFile);
|
||||
}
|
||||
|
||||
Horizon::$nonceAttribute = '';
|
||||
Facade::clearResolvedInstances();
|
||||
Container::setInstance($this->originalContainer);
|
||||
Facade::setFacadeApplication($this->originalContainer);
|
||||
Facade::setFacadeApplication($this->originalFacadeApplication);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
@@ -59,4 +76,52 @@ class ContentSecurityPolicyTest extends TestCase
|
||||
$this->assertStringContainsString('https://fonts.bunny.net', (string) $response->headers->get('Content-Security-Policy'));
|
||||
$this->assertSame(4, substr_count((string) $response->getContent(), 'nonce="'.$nonce.'"'));
|
||||
}
|
||||
|
||||
public function test_local_vite_hot_reload_sources_are_added_to_the_csp(): void
|
||||
{
|
||||
config()->set('app.env', 'local');
|
||||
file_put_contents($this->hotFile, 'http://localhost:5173');
|
||||
|
||||
$response = (new ContentSecurityPolicy)->handle(
|
||||
Request::create('/'),
|
||||
static fn (): Response => new Response,
|
||||
);
|
||||
|
||||
$csp = (string) $response->headers->get('Content-Security-Policy');
|
||||
|
||||
$this->assertStringContainsString("style-src-elem 'self' 'unsafe-inline'", $csp);
|
||||
$this->assertStringContainsString('https://cdn.tiny.cloud http://localhost:5173', $csp);
|
||||
$this->assertStringContainsString('https://sp.tinymce.com http://localhost:5173 ws://localhost:5173', $csp);
|
||||
}
|
||||
|
||||
public function test_vite_hot_reload_sources_are_not_added_outside_local_environment(): void
|
||||
{
|
||||
file_put_contents($this->hotFile, 'http://localhost:5173');
|
||||
|
||||
$response = (new ContentSecurityPolicy)->handle(
|
||||
Request::create('/'),
|
||||
static fn (): Response => new Response,
|
||||
);
|
||||
|
||||
$this->assertStringNotContainsString(
|
||||
'http://localhost:5173',
|
||||
(string) $response->headers->get('Content-Security-Policy'),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_non_loopback_vite_hot_reload_sources_are_rejected(): void
|
||||
{
|
||||
config()->set('app.env', 'local');
|
||||
file_put_contents($this->hotFile, 'https://assets.example.com:5173');
|
||||
|
||||
$response = (new ContentSecurityPolicy)->handle(
|
||||
Request::create('/'),
|
||||
static fn (): Response => new Response,
|
||||
);
|
||||
|
||||
$this->assertStringNotContainsString(
|
||||
'https://assets.example.com:5173',
|
||||
(string) $response->headers->get('Content-Security-Policy'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LayoutShellMarkupTest extends TestCase
|
||||
{
|
||||
public function test_user_and_admin_layouts_expose_accessible_shell_landmarks(): void
|
||||
{
|
||||
foreach (['main', 'admin'] as $layout) {
|
||||
$markup = $this->view("layouts/{$layout}.blade.php");
|
||||
|
||||
$this->assertStringContainsString('class="skip-link"', $markup);
|
||||
$this->assertStringContainsString('class="layout-shell', $markup);
|
||||
$this->assertStringContainsString('class="layout-sidebar', $markup);
|
||||
$this->assertStringContainsString('class="layout-topbar', $markup);
|
||||
$this->assertStringContainsString('id="main-content"', $markup);
|
||||
$this->assertStringContainsString('id="mobile-sidebar-backdrop"', $markup);
|
||||
$this->assertStringContainsString('aria-controls="sidebar"', $markup);
|
||||
$this->assertStringContainsString('aria-expanded="false"', $markup);
|
||||
}
|
||||
|
||||
$this->assertStringNotContainsString('assets/images/logo.svg', $this->view('layouts/main.blade.php'));
|
||||
}
|
||||
|
||||
public function test_mobile_sidebar_script_supports_dismissal_and_focus_state(): void
|
||||
{
|
||||
$script = $this->resource('js/alpine/components/mobile-nav.js');
|
||||
|
||||
$this->assertStringContainsString("event.key === 'Escape'", $script);
|
||||
$this->assertStringContainsString("mobileSidebarBackdrop?.addEventListener('click'", $script);
|
||||
$this->assertStringContainsString("mobileSidebarToggle.setAttribute('aria-expanded'", $script);
|
||||
$this->assertStringContainsString('mobileSidebarClose?.focus()', $script);
|
||||
}
|
||||
|
||||
public function test_shared_shell_styles_include_focus_and_reduced_motion_support(): void
|
||||
{
|
||||
$stylesheet = $this->resource('css/app.css');
|
||||
|
||||
$this->assertStringContainsString(':focus-visible', $stylesheet);
|
||||
$this->assertStringContainsString('@media (prefers-reduced-motion: reduce)', $stylesheet);
|
||||
$this->assertStringContainsString('.layout-flash--success', $stylesheet);
|
||||
$this->assertStringContainsString('.admin-page-header__icon', $stylesheet);
|
||||
}
|
||||
|
||||
public function test_shared_icon_only_controls_have_accessible_names(): void
|
||||
{
|
||||
$this->assertStringContainsString('aria-label="Search releases"', $this->view('partials/header-menu.blade.php'));
|
||||
$this->assertStringContainsString('aria-label="Close confirmation"', $this->view('partials/confirmation-modal.blade.php'));
|
||||
$this->assertStringContainsString('aria-label="Dismiss notification"', $this->view('partials/toast-notifications.blade.php'));
|
||||
}
|
||||
|
||||
private function view(string $path): string
|
||||
{
|
||||
return $this->resource("views/{$path}");
|
||||
}
|
||||
|
||||
private function resource(string $path): string
|
||||
{
|
||||
return (string) file_get_contents(__DIR__."/../../resources/{$path}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ViteDockerConfigurationTest extends TestCase
|
||||
{
|
||||
public function test_vite_uses_the_docker_published_port_without_fallback(): void
|
||||
{
|
||||
$viteConfig = $this->projectFile('vite.config.js');
|
||||
$composeConfig = $this->projectFile('docker-compose.yml');
|
||||
|
||||
$this->assertStringContainsString("loadEnv(mode, process.cwd(), '')", $viteConfig);
|
||||
$this->assertStringContainsString("environment.VITE_PORT || '5173'", $viteConfig);
|
||||
$this->assertStringContainsString("environment.VITE_DEV_SERVER_HOST || 'localhost'", $viteConfig);
|
||||
$this->assertStringContainsString("host: '0.0.0.0'", $viteConfig);
|
||||
$this->assertStringContainsString('strictPort: true', $viteConfig);
|
||||
$this->assertStringContainsString('origin: `http://${viteDevServerHost}:${vitePort}`', $viteConfig);
|
||||
$this->assertStringContainsString("'\${VITE_PORT:-5173}:\${VITE_PORT:-5173}'", $composeConfig);
|
||||
$this->assertStringContainsString('VITE_DEV_SERVER_HOST=localhost', $this->projectFile('.env.example'));
|
||||
}
|
||||
|
||||
public function test_container_startup_clears_stale_vite_hot_state(): void
|
||||
{
|
||||
$startContainer = $this->projectFile('docker/8.5/start-container');
|
||||
|
||||
$unlinkPosition = strpos($startContainer, 'unlink /var/www/html/public/hot');
|
||||
$supervisorPosition = strpos($startContainer, 'exec /usr/bin/supervisord');
|
||||
|
||||
$this->assertNotFalse($unlinkPosition);
|
||||
$this->assertNotFalse($supervisorPosition);
|
||||
$this->assertLessThan($supervisorPosition, $unlinkPosition);
|
||||
}
|
||||
|
||||
private function projectFile(string $path): string
|
||||
{
|
||||
return (string) file_get_contents(__DIR__."/../../{$path}");
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -1,8 +1,23 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => {
|
||||
const environment = loadEnv(mode, process.cwd(), '');
|
||||
const vitePort = Number.parseInt(environment.VITE_PORT || '5173', 10);
|
||||
const viteDevServerHost = environment.VITE_DEV_SERVER_HOST || 'localhost';
|
||||
|
||||
return {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: vitePort,
|
||||
strictPort: true,
|
||||
origin: `http://${viteDevServerHost}:${vitePort}`,
|
||||
hmr: {
|
||||
host: viteDevServerHost,
|
||||
port: vitePort,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
laravel({
|
||||
@@ -15,4 +30,5 @@ export default defineConfig({
|
||||
refresh: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user