- Add x-navigate directive to all sidebar document links for Alpine navigation - Store scroll position per page using URL path as key in sessionStorage - Each page now maintains its own scroll position in the sidebar - Save scroll position before navigation and restore after navigation - Scroll position is preserved when clicking links in the sidebar - Works correctly with Alpine navigate events triggered by x-navigate directive
370 lines
20 KiB
PHP
370 lines
20 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
|
|
<title>{{ $title ?? config('app.name', 'Knowledge Base') }}</title>
|
|
|
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
|
<script>document.addEventListener('livewire:navigated', function() { hljs.highlightAll(); });</script>
|
|
<style>
|
|
pre code.hljs {
|
|
background: #1e1e1e !important;
|
|
color: #dcdcdc !important;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
display: block;
|
|
overflow-x: auto;
|
|
}
|
|
</style>
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
@livewireStyles
|
|
@stack('styles')
|
|
</head>
|
|
<body class="font-sans antialiased" x-data="{
|
|
mobileMenuOpen: false,
|
|
sidebarWidth: localStorage.getItem('kb_sidebar_width') || 320,
|
|
isResizing: false,
|
|
startResize(e) {
|
|
if (window.innerWidth < 1024) return; // lg breakpoint
|
|
this.isResizing = true;
|
|
document.body.style.cursor = 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
},
|
|
resize(e) {
|
|
if (!this.isResizing) return;
|
|
const newWidth = Math.max(200, Math.min(600, e.clientX));
|
|
this.sidebarWidth = newWidth;
|
|
localStorage.setItem('kb_sidebar_width', newWidth);
|
|
},
|
|
stopResize() {
|
|
if (this.isResizing) {
|
|
this.isResizing = false;
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
}
|
|
}
|
|
}" @mousemove.window="resize($event)" @mouseup.window="stopResize()">
|
|
<div class="min-h-screen bg-gray-50">
|
|
<!-- Header -->
|
|
<header class="bg-white border-b border-gray-200 sticky top-0 z-20">
|
|
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-16">
|
|
<div class="flex items-center space-x-3">
|
|
<!-- Mobile Menu Toggle -->
|
|
<button
|
|
@click="mobileMenuOpen = !mobileMenuOpen"
|
|
class="lg:hidden p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
aria-label="Toggle menu"
|
|
>
|
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path x-show="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
<path x-show="mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<a href="{{ url('/') }}" class="flex items-center space-x-2 sm:space-x-3">
|
|
<x-application-logo class="block h-6 sm:h-8 w-auto fill-current text-gray-800" />
|
|
<h1 class="text-lg sm:text-xl font-semibold text-gray-900 hidden xs:block">
|
|
{{ config('app.name', 'Knowledge Base') }}
|
|
</h1>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2 sm:space-x-4">
|
|
<!-- Quick Switcher Trigger -->
|
|
<button
|
|
type="button"
|
|
class="inline-flex items-center px-2 sm:px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
x-data
|
|
@click.prevent="$dispatch('open-quick-switcher')"
|
|
>
|
|
<svg class="h-4 w-4 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
<span class="hidden sm:inline">{{ __('messages.quick_switcher.title') }}</span>
|
|
<kbd class="hidden md:inline-flex ml-2 px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded">
|
|
Ctrl+K
|
|
</kbd>
|
|
</button>
|
|
|
|
<!-- Language Switcher (for all users) -->
|
|
<div x-data="{ open: false }" @click.away="open = false" class="relative">
|
|
<button
|
|
@click="open = !open"
|
|
class="flex items-center px-2 sm:px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none"
|
|
title="{{ __('messages.settings.change_language') }}"
|
|
>
|
|
<svg class="w-4 h-4 sm:w-5 sm:h-5 sm:mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"></path>
|
|
</svg>
|
|
@php
|
|
$currentLocale = app()->getLocale();
|
|
$locales = \App\Http\Middleware\SetLocale::SUPPORTED_LOCALES;
|
|
@endphp
|
|
<span class="hidden sm:inline">{{ $locales[$currentLocale] ?? 'English' }}</span>
|
|
<svg class="ml-1 h-4 w-4 hidden sm:block" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<div
|
|
x-show="open"
|
|
x-transition
|
|
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 max-h-96 overflow-y-auto z-50"
|
|
>
|
|
@foreach($locales as $code => $name)
|
|
<form method="POST" action="{{ route('locale.update') }}" class="inline-block w-full">
|
|
@csrf
|
|
<input type="hidden" name="locale" value="{{ $code }}">
|
|
<button
|
|
type="submit"
|
|
class="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 {{ $currentLocale === $code ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-gray-700' }}"
|
|
>
|
|
{{ $name }}
|
|
@if($currentLocale === $code)
|
|
<svg class="inline-block w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
@endif
|
|
</button>
|
|
</form>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
@auth
|
|
<!-- User Dropdown -->
|
|
<div x-data="{ open: false }" @click.away="open = false" class="relative">
|
|
<button
|
|
@click="open = !open"
|
|
class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none"
|
|
>
|
|
<span class="hidden md:inline">{{ Auth::user()->name }}</span>
|
|
<span class="md:hidden w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-700 font-semibold">
|
|
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
|
|
</span>
|
|
<svg class="ml-1 h-4 w-4 hidden md:block" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<div
|
|
x-show="open"
|
|
x-transition
|
|
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50"
|
|
>
|
|
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
{{ __('messages.nav.profile') }}
|
|
</a>
|
|
@if(Auth::user()->isAdmin())
|
|
<a href="{{ route('admin.users.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<span class="flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
</svg>
|
|
{{ __('messages.nav.user_management') }}
|
|
</span>
|
|
</a>
|
|
@endif
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
{{ __('messages.nav.logout') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900 hidden sm:block">
|
|
{{ __('messages.nav.login') }}
|
|
</a>
|
|
<a href="{{ route('login') }}" class="sm:hidden p-2 text-gray-700 hover:bg-gray-100 rounded-md" title="{{ __('messages.nav.login') }}">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
|
|
</svg>
|
|
</a>
|
|
@endauth
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<div class="flex h-[calc(100vh-4rem)]">
|
|
<!-- Sidebar - Desktop -->
|
|
<aside
|
|
id="kb-sidebar"
|
|
class="hidden lg:block bg-white border-r border-gray-200 overflow-y-auto relative"
|
|
:style="'width: ' + sidebarWidth + 'px'"
|
|
>
|
|
@livewire('sidebar-tree')
|
|
|
|
<!-- Resize Handle -->
|
|
<div
|
|
@mousedown="startResize($event)"
|
|
class="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-indigo-500 transition-colors group"
|
|
title="ドラッグして幅を変更"
|
|
>
|
|
<div class="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 w-1.5 h-12 bg-gray-300 rounded-full group-hover:bg-indigo-500 transition-colors"></div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Sidebar - Mobile Overlay -->
|
|
<div
|
|
x-show="mobileMenuOpen"
|
|
@click="mobileMenuOpen = false"
|
|
x-transition:enter="transition-opacity ease-linear duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition-opacity ease-linear duration-300"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 bg-gray-600 bg-opacity-75 z-30 lg:hidden"
|
|
style="display: none;"
|
|
></div>
|
|
|
|
<aside
|
|
x-show="mobileMenuOpen"
|
|
@click.away="mobileMenuOpen = false"
|
|
x-transition:enter="transition ease-in-out duration-300 transform"
|
|
x-transition:enter-start="-translate-x-full"
|
|
x-transition:enter-end="translate-x-0"
|
|
x-transition:leave="transition ease-in-out duration-300 transform"
|
|
x-transition:leave-start="translate-x-0"
|
|
x-transition:leave-end="-translate-x-full"
|
|
class="fixed inset-y-0 left-0 top-16 w-64 bg-white border-r border-gray-200 overflow-y-auto z-40 lg:hidden"
|
|
style="display: none;"
|
|
>
|
|
@livewire('sidebar-tree')
|
|
</aside>
|
|
|
|
<!-- Main Panel -->
|
|
<main class="flex-1 overflow-y-auto">
|
|
{{ $slot }}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Switcher Modal -->
|
|
@livewire('quick-switcher')
|
|
|
|
@livewireScripts
|
|
|
|
<!-- Global Keyboard Shortcuts -->
|
|
<script>
|
|
// Sidebar scroll position management per page
|
|
function getSidebarScrollKey() {
|
|
// Use current page URL to create a unique key for scroll position
|
|
return 'kb_sidebar_scroll_' + window.location.pathname;
|
|
}
|
|
|
|
function saveSidebarScroll() {
|
|
const sidebar = document.getElementById('kb-sidebar');
|
|
if (sidebar) {
|
|
const scrollPos = sidebar.scrollTop;
|
|
const key = getSidebarScrollKey();
|
|
sessionStorage.setItem(key, scrollPos);
|
|
console.log('Saved sidebar scroll for ' + window.location.pathname + ':', scrollPos);
|
|
}
|
|
}
|
|
|
|
function restoreSidebarScroll() {
|
|
const sidebar = document.getElementById('kb-sidebar');
|
|
if (!sidebar) return;
|
|
|
|
// Use requestAnimationFrame to ensure DOM is fully rendered
|
|
requestAnimationFrame(() => {
|
|
const key = getSidebarScrollKey();
|
|
const savedPos = sessionStorage.getItem(key);
|
|
console.log('Retrieved from sessionStorage for ' + window.location.pathname + ':', savedPos);
|
|
|
|
if (savedPos !== null && parseInt(savedPos, 10) > 0) {
|
|
const pos = parseInt(savedPos, 10);
|
|
sidebar.scrollTop = pos;
|
|
console.log('Restored sidebar scroll for ' + window.location.pathname + ' to:', pos);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Intercept sidebar link clicks
|
|
document.addEventListener('click', function(e) {
|
|
const sidebar = document.getElementById('kb-sidebar');
|
|
if (!sidebar) return;
|
|
|
|
const link = e.target.closest('a');
|
|
if (sidebar.contains(link)) {
|
|
console.log('Sidebar link clicked, saving scroll');
|
|
saveSidebarScroll();
|
|
}
|
|
}, true);
|
|
|
|
// Save before Alpine navigation
|
|
document.addEventListener('alpine:navigating', saveSidebarScroll);
|
|
|
|
// Restore after Alpine navigation
|
|
document.addEventListener('alpine:navigated', () => {
|
|
console.log('Alpine navigated event fired, attempting restore');
|
|
restoreSidebarScroll();
|
|
});
|
|
|
|
// For page load/reload
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('DOMContentLoaded - calling restoreSidebarScroll');
|
|
restoreSidebarScroll();
|
|
});
|
|
} else {
|
|
console.log('Document already loaded - calling restoreSidebarScroll');
|
|
setTimeout(restoreSidebarScroll, 100);
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
window.dispatchEvent(new CustomEvent('open-quick-switcher'));
|
|
}
|
|
});
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('sidebarState', () => ({
|
|
expandedFolders: [],
|
|
|
|
initExpandedFolders() {
|
|
const stored = localStorage.getItem('kb_expanded_folders');
|
|
if (stored) {
|
|
try {
|
|
this.expandedFolders = JSON.parse(stored);
|
|
} catch (e) {
|
|
this.expandedFolders = [];
|
|
}
|
|
}
|
|
},
|
|
|
|
toggleFolder(path) {
|
|
const index = this.expandedFolders.indexOf(path);
|
|
if (index > -1) {
|
|
this.expandedFolders.splice(index, 1);
|
|
} else {
|
|
this.expandedFolders.push(path);
|
|
}
|
|
localStorage.setItem('kb_expanded_folders', JSON.stringify(this.expandedFolders));
|
|
},
|
|
|
|
isFolderExpanded(path) {
|
|
return this.expandedFolders.includes(path);
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
|
|
@stack('scripts')
|
|
</body>
|
|
</html>
|