Compare commits

..

17 Commits

Author SHA1 Message Date
8ea8b3f6b6 Improve current document highlighting with better URL matching
- Add removal of previous highlighting before applying new one
- Support multiple URL matching strategies (exact, full URL, ends with)
- Add debug logging to troubleshoot highlighting issues
- Reset all links before applying highlight to ensure clean state
2025-12-04 02:45:24 +09:00
d52968e697 Add current document highlighting in sidebar navigation
- Highlight active document with indigo background and bold font
- Change icon color to indigo for active document
- Use JavaScript to match current URL path with sidebar links
- Update highlighting on page load and Alpine navigation events
- Active document is visually distinct from other items
2025-12-04 02:42:20 +09:00
bed7137e43 Remove debug console logs from sidebar scroll preservation
- Remove all console.log statements
- Clean up code for production use
- Preserve scroll position functionality without debug output
2025-12-04 02:37:07 +09:00
028e0b11c7 Fix sidebar scroll preservation by correcting link selector
- Change selector from 'a[x-navigate]' to 'a' (no x-navigate attribute)
- Remove sessionStorage.removeItem to prevent clearing saved scroll position
- Add '0' check to prevent restoring scroll position to top
- Add debug logging for troubleshooting
- Now works correctly in all browsers including Chrome
2025-12-04 02:35:29 +09:00
5bf43abab9 Remove x-navigate directive, use native HTML links with scroll preservation
- Remove x-navigate.preserve-scroll from sidebar links
- Use standard <a> tags for navigation
- Implement manual JavaScript-based scroll position management
- Save scroll position to sessionStorage before navigation
- Restore scroll position after page load
- Works consistently across all browsers including Chrome
2025-12-04 02:30:48 +09:00
f96ad4d14f Use manual scroll position management instead of x-navigate.preserve-scroll
- Add JavaScript to save sidebar scroll position before navigation
- Restore scroll position after page load using sessionStorage
- Works consistently in Chrome and other browsers
- Handles both DOMContentLoaded and window load events
- Compatible with Alpine navigate and standard navigation
2025-12-04 02:24:00 +09:00
a4aff43091 Add preserve-scroll modifier to x-navigate directive
- Use x-navigate.preserve-scroll to maintain sidebar scroll position during navigation
- Prevents page from scrolling to top after clicking sidebar links
- Alpine navigate automatically saves and restores scroll position
2025-12-04 02:17:13 +09:00
1e20982e00 Simplify sidebar scroll preservation using only x-navigate directive
- Remove custom sessionStorage scroll management logic
- Rely solely on x-navigate directive from Alpine for scroll preservation
- x-navigate handles automatic scroll position saving and restoring
- Cleaner and simpler implementation
- Keep x-navigate directive on all sidebar links
2025-12-04 02:13:28 +09:00
ec7aaf44a9 Fix sidebar scroll preservation per page with x-navigate directive
- 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
2025-12-04 02:11:06 +09:00
00a5951654 Improve sidebar scroll position preservation with sessionStorage fallback
- Replace localStorage with sessionStorage for session-based scroll restoration
- Add console logging for debugging scroll behavior
- Support both Livewire and Alpine navigate events
- Intercept sidebar link clicks to ensure scroll position is saved before navigation
- Use setTimeout for smoother DOM restoration timing
- Restore scroll position on page load and window load events
- Sidebar now maintains scroll position consistently across navigation
2025-12-04 01:55:59 +09:00
8dba510a6c Fix sidebar scroll position preservation during page navigation
- Replace unstable x-navigate:scroll directive with custom Alpine event handlers
- Use alpine:navigating event to save sidebar scroll position to localStorage
- Use alpine:navigated event to restore sidebar scroll position after navigation
- Sidebar now maintains scroll position when clicking document links
- Fixed 'Element not found' error that was preventing scroll restoration
- Uses requestAnimationFrame for smooth DOM restoration
2025-12-04 01:47:51 +09:00
e66ece71e3 Preserve sidebar scroll position when navigating between documents
- Add x-navigate:scroll directive to sidebar container to maintain scroll position
- Add x-navigate:scroll to all document links in sidebar (tree-item.blade.php)
- Add x-navigate:scroll to 'New Document' button
- When clicking a link in sidebar, the sidebar scroll position is now preserved during page navigation
- Fixes issue where sidebar would scroll to top after loading a document
2025-12-04 01:37:19 +09:00
b96012f598 Add resizable sidebar feature and increase default width to 320px
- Add resizable handle on the right edge of sidebar in desktop view (lg and above)
- Allow dragging to adjust sidebar width between 200px and 600px
- Persist resize settings in localStorage across page reloads
- Keep mobile mode unchanged with fixed 256px width
- Increase default width from 256px to 320px
- Implement visual feedback with color change on mouse hover
2025-12-04 01:24:47 +09:00
e50ed261e1 Show timezone information 2025-11-30 13:58:03 +09:00
79a09430aa Fix timezone issue: Set default timezone to Asia/Tokyo
- Update config/app.php to use APP_TIMEZONE from .env with Asia/Tokyo as default
- Add APP_TIMEZONE to .env.example
- Fixes issue where timestamps were displayed 9 hours behind (UTC vs JST)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:46:16 +09:00
33fef93ce0 Update README with comprehensive project documentation
- Add detailed feature overview (wiki-links, folders, multi-language, responsive)
- Include complete installation and setup instructions
- Document project structure and key concepts
- Add development workflow and common commands
- Include troubleshooting section
- Update technology stack and credits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:19:43 +09:00
e14cc5dd43 Implement responsive design for mobile and tablet devices
- Add hamburger menu for mobile sidebar with slide-out animation
- Make header navigation responsive with icon-only buttons on mobile
- Adjust document viewer, editor, and quick switcher layouts for smaller screens
- Preserve all existing functionality including localStorage folder state persistence
- Use Tailwind responsive utilities (sm:, md:, lg:) for progressive enhancement

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:17:50 +09:00
7 changed files with 543 additions and 98 deletions

View File

@@ -7,6 +7,7 @@ APP_URL=http://localhost
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US
APP_TIMEZONE=Asia/Tokyo
APP_MAINTENANCE_DRIVER=file APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database # APP_MAINTENANCE_STORE=database

View File

@@ -1,59 +1,332 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p> # Knowledge Base System
<p align="center"> An Obsidian-like knowledge base system built with Laravel 12, Livewire v3, and Alpine.js. Create, organize, and link your documents with wiki-style references and a powerful search interface.
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel ## Features
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: ### Core Functionality
- **Markdown-based documents** with live preview using EasyMDE editor
- **Wiki-style linking** with `[[Document Title]]` syntax
- **Automatic backlinks** - see which documents reference the current page
- **Folder organization** - use `/` in titles to auto-organize into folders (e.g., `Laravel/Livewire/Components`)
- **Quick switcher** - Press `Ctrl+K` to instantly search and navigate
- **Full-text search** - MySQL FULLTEXT index with ngram tokenizer for multilingual support
- **ID-based routing** - Clean URLs with guaranteed uniqueness
- [Simple, fast routing engine](https://laravel.com/docs/routing). ### Multi-Language Support
- [Powerful dependency injection container](https://laravel.com/docs/container). Interface available in **16 languages**:
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. - English, 日本語 (Japanese)
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). - 简体中文, 繁體中文 (Simplified/Traditional Chinese)
- Database agnostic [schema migrations](https://laravel.com/docs/migrations). - 한국어 (Korean)
- [Robust background job processing](https://laravel.com/docs/queues). - हिन्दी (Hindi), Tiếng Việt (Vietnamese), Türkçe (Turkish)
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). - Deutsch, Français, Español, Português (Brasil)
- Русский, Українська, Italiano, Polski
Laravel is accessible, powerful, and provides tools required for large, robust applications. Language preferences persist for both authenticated and guest users via cookies.
## Learning Laravel ### Responsive Design
- **Mobile-first** interface with hamburger menu
- **Tablet and desktop** optimized layouts
- **Touch-friendly** navigation
- All features work seamlessly across devices
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. ### User Management
- **Role-based access** - Admin and regular user roles
- **User authentication** - Laravel Breeze integration
- **Profile management** - Update name, email, password, and language preference
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. ## Technology Stack
## Laravel Sponsors - **Backend**: Laravel 12.0 (PHP 8.2+)
- **Frontend**: Livewire v3.7.0, Alpine.js, Tailwind CSS
- **Database**: MySQL 8.0 with FULLTEXT indexing
- **Markdown**: league/commonmark for rendering
- **Editor**: EasyMDE (markdown editor)
- **Docker**: Custom containerized environment
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). ## Prerequisites
### Premium Partners - Docker and Docker Compose
- Node.js 18+ (for asset compilation)
- Git
- **[Vehikl](https://vehikl.com)** ## Installation
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** ### 1. Clone the repository
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)** ```bash
- **[DevSquad](https://devsquad.com/hire-laravel-developers)** git clone <repository-url>
- **[Redberry](https://redberry.international/laravel-development)** cd knowledge-base
- **[Active Logic](https://activelogic.com)** ```
### 2. Start Docker services
```bash
docker compose up -d
```
This starts:
- Nginx: `http://localhost:9700`
- phpMyAdmin: `http://localhost:9701`
- MySQL: `localhost:9702`
- MailHog: `http://localhost:9725`
### 3. Install dependencies
```bash
# Inside the src/ directory
cd src
# Install PHP dependencies
docker compose exec php composer install
# Install Node dependencies
npm install
```
### 4. Configure environment
```bash
# Copy environment file
cp .env.example .env
# Generate application key
docker compose exec php php artisan key:generate
```
### 5. Set up database
```bash
# Run migrations
docker compose exec php php artisan migrate
# Seed initial user (admin@example.com / password)
docker compose exec php php artisan db:seed --class=UserSeeder
# Initialize sample documents (optional)
docker compose exec php php artisan docs:init
```
### 6. Build frontend assets
```bash
# Development mode with hot reload
npm run dev
# Or production build
npm run build
```
### 7. Access the application
Open `http://localhost:9700` in your browser.
**Default credentials**:
- Email: `admin@example.com`
- Password: `password`
## Development
### Running the dev environment
```bash
# Start all services (server, queue, logs, Vite)
docker compose exec php composer dev
```
### Running tests
```bash
docker compose exec php php artisan test
```
### Common commands
```bash
# Access PHP container shell
docker compose exec php bash
# Clear caches
docker compose exec php php artisan config:clear
docker compose exec php php artisan cache:clear
docker compose exec php php artisan view:clear
# Publish Livewire assets (after updates)
docker compose exec php php artisan livewire:publish --assets
```
## Project Structure
```
src/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ └── LocaleController.php # Language switching
│ │ └── Middleware/
│ │ └── SetLocale.php # Multi-language support
│ ├── Livewire/
│ │ ├── DocumentEditor.php # Create/edit documents
│ │ ├── DocumentViewer.php # Display documents
│ │ ├── QuickSwitcher.php # Ctrl+K search modal
│ │ └── SidebarTree.php # Folder tree navigation
│ ├── Models/
│ │ ├── Document.php # Document model
│ │ ├── DocumentLink.php # Wiki-style links
│ │ └── RecentDocument.php # Access history
│ └── Services/
│ └── DocumentService.php # Document business logic
├── database/
│ └── migrations/ # Database schema
├── lang/ # Translation files (16 languages)
├── resources/
│ ├── css/
│ │ └── app.css # Tailwind + custom styles
│ ├── js/
│ │ └── app.js # Alpine.js initialization
│ └── views/
│ ├── layouts/
│ │ └── knowledge-base.blade.php # Main layout
│ └── livewire/ # Livewire component views
└── routes/
└── web.php # Application routes
```
## Key Concepts
### Document Organization
Documents are organized using **virtual paths** derived from titles:
```php
Title: "Laravel/Livewire/Components"
→ Path: "Laravel/Livewire/Components.md"
→ Slug: "components"
→ Sidebar: Nested under Laravel → Livewire → Components
```
No manual directory field needed - just use `/` in the title!
### Wiki-Style Links
Create links between documents using double brackets:
```markdown
See [[Getting Started]] for more information.
Links to [[Uncreated Pages]] appear in red.
```
Links are automatically:
- Extracted and stored in the `document_links` table
- Rendered as clickable HTML anchors
- Displayed as backlinks on target documents
### ID-Based Routing
URLs use document IDs instead of slugs:
```
/documents/123 (instead of /documents/my-document-slug)
```
Benefits:
- Guaranteed uniqueness
- Title changes don't break URLs
- Simpler route model binding
### Folder State Persistence
Sidebar folder expanded/collapsed state is stored in `localStorage`:
```javascript
// Managed by Alpine.js
localStorage.getItem('kb_expanded_folders')
// ["Laravel", "Laravel/Livewire", "Docker"]
```
Survives page navigation and browser sessions.
## Customization
### Adding new languages
1. Add to `SetLocale::SUPPORTED_LOCALES` in `app/Http/Middleware/SetLocale.php`
2. Create translation file at `lang/{code}/messages.php`
3. Copy structure from existing language file
### Changing default locale
Edit `config/app.php`:
```php
'locale' => 'en', // Change to your preferred language code
```
### Customizing markdown styles
Edit `resources/css/app.css`:
```css
@layer components {
.prose .wiki-link {
@apply text-indigo-600 hover:text-indigo-800 underline;
}
}
```
## Troubleshooting
### Livewire assets not loading
```bash
docker compose exec php php artisan livewire:publish --assets
```
### Frontend changes not reflecting
```bash
npm run build
docker compose exec php php artisan view:clear
```
### Database connection errors
Check `.env` file matches Docker Compose settings:
```env
DB_CONNECTION=mysql
DB_HOST=kb_mysql
DB_PORT=3306
DB_DATABASE=knowledge_base
DB_USERNAME=kb_user
DB_PASSWORD=kb_password
```
### Alpine.js errors in console
Ensure scripts are loaded in correct order in `knowledge-base.blade.php`:
1. Livewire scripts first
2. Alpine.js initialization (via Vite)
3. Custom Alpine components
## Contributing ## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). Contributions are welcome! Please ensure:
- Code follows Laravel and PSR-12 conventions
## Code of Conduct - All existing tests pass
- New features include tests
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). - UI changes maintain responsive design
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License ## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
## Credits
Built with:
- [Laravel](https://laravel.com) - The PHP Framework
- [Livewire](https://livewire.laravel.com) - Full-stack framework for Laravel
- [Alpine.js](https://alpinejs.dev) - Lightweight JavaScript framework
- [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework
- [EasyMDE](https://github.com/Ionaru/easy-markdown-editor) - Markdown editor
- [league/commonmark](https://commonmark.thephpleague.com) - Markdown parser

View File

@@ -65,7 +65,7 @@
| |
*/ */
'timezone' => 'UTC', 'timezone' => env('APP_TIMEZONE', 'Asia/Tokyo'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -27,34 +27,69 @@
@livewireStyles @livewireStyles
@stack('styles') @stack('styles')
</head> </head>
<body class="font-sans antialiased"> <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"> <div class="min-h-screen bg-gray-50">
<!-- Header --> <!-- Header -->
<header class="bg-white border-b border-gray-200 sticky top-0 z-10"> <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="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16"> <div class="flex justify-between h-16">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<a href="{{ url('/') }}" class="flex items-center space-x-3"> <!-- Mobile Menu Toggle -->
<x-application-logo class="block h-8 w-auto fill-current text-gray-800" /> <button
<h1 class="text-xl font-semibold text-gray-900"> @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') }} {{ config('app.name', 'Knowledge Base') }}
</h1> </h1>
</a> </a>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-2 sm:space-x-4">
<!-- Quick Switcher Trigger --> <!-- Quick Switcher Trigger -->
<button <button
type="button" type="button"
class="inline-flex items-center 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" 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 x-data
@click.prevent="$dispatch('open-quick-switcher')" @click.prevent="$dispatch('open-quick-switcher')"
> >
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> <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> </svg>
{{ __('messages.quick_switcher.title') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.title') }}</span>
<kbd class="ml-2 px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded"> <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 Ctrl+K
</kbd> </kbd>
</button> </button>
@@ -63,10 +98,10 @@ class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-
<div x-data="{ open: false }" @click.away="open = false" class="relative"> <div x-data="{ open: false }" @click.away="open = false" class="relative">
<button <button
@click="open = !open" @click="open = !open"
class="flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none" 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') }}" title="{{ __('messages.settings.change_language') }}"
> >
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> <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> </svg>
@php @php
@@ -74,7 +109,7 @@ class="flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-
$locales = \App\Http\Middleware\SetLocale::SUPPORTED_LOCALES; $locales = \App\Http\Middleware\SetLocale::SUPPORTED_LOCALES;
@endphp @endphp
<span class="hidden sm:inline">{{ $locales[$currentLocale] ?? 'English' }}</span> <span class="hidden sm:inline">{{ $locales[$currentLocale] ?? 'English' }}</span>
<svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <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> <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> </svg>
</button> </button>
@@ -82,7 +117,7 @@ class="flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-
<div <div
x-show="open" x-show="open"
x-transition 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" 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) @foreach($locales as $code => $name)
<form method="POST" action="{{ route('locale.update') }}" class="inline-block w-full"> <form method="POST" action="{{ route('locale.update') }}" class="inline-block w-full">
@@ -111,8 +146,11 @@ class="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 {{ $currentLocale ==
@click="open = !open" @click="open = !open"
class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none" class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none"
> >
{{ Auth::user()->name }} <span class="hidden md:inline">{{ Auth::user()->name }}</span>
<svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <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> <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> </svg>
</button> </button>
@@ -120,7 +158,7 @@ class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 f
<div <div
x-show="open" x-show="open"
x-transition 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" 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"> <a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
{{ __('messages.nav.profile') }} {{ __('messages.nav.profile') }}
@@ -144,9 +182,14 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
</div> </div>
</div> </div>
@else @else
<a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900"> <a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900 hidden sm:block">
{{ __('messages.nav.login') }} {{ __('messages.nav.login') }}
</a> </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 @endauth
</div> </div>
</div> </div>
@@ -155,8 +198,50 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
<!-- Main Content --> <!-- Main Content -->
<div class="flex h-[calc(100vh-4rem)]"> <div class="flex h-[calc(100vh-4rem)]">
<!-- Sidebar --> <!-- Sidebar - Desktop -->
<aside class="w-64 bg-white border-r border-gray-200 overflow-y-auto"> <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') @livewire('sidebar-tree')
</aside> </aside>
@@ -174,6 +259,91 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
<!-- Global Keyboard Shortcuts --> <!-- Global Keyboard Shortcuts -->
<script> <script>
// Preserve sidebar scroll position during navigation
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('kb-sidebar');
if (!sidebar) return;
const link = e.target.closest('a');
if (link && sidebar.contains(link)) {
const scrollPos = sidebar.scrollTop;
sessionStorage.setItem('kb_sidebar_scroll', scrollPos);
}
}, true);
// Restore scroll position after page load
function restoreSidebarScroll() {
const sidebar = document.getElementById('kb-sidebar');
if (!sidebar) return;
const savedPos = sessionStorage.getItem('kb_sidebar_scroll');
if (savedPos !== null && savedPos !== '0') {
sidebar.scrollTop = parseInt(savedPos, 10);
}
}
// Highlight current document in sidebar
function highlightCurrentDocument() {
const sidebar = document.getElementById('kb-sidebar');
if (!sidebar) {
console.log('Sidebar not found for highlighting');
return;
}
const currentPath = window.location.pathname;
const links = sidebar.querySelectorAll('a');
console.log('Current path:', currentPath);
console.log('Found links in sidebar:', links.length);
links.forEach(link => {
const href = link.getAttribute('href');
// Remove previous highlighting
link.classList.remove('bg-indigo-50', 'text-indigo-700', 'font-semibold');
link.classList.add('text-gray-700');
const icon = link.querySelector('svg');
if (icon) {
icon.classList.remove('text-indigo-600');
icon.classList.add('text-gray-400', 'group-hover:text-gray-600');
}
// Check if this is the current page
if (href === currentPath || href === window.location.href ||
(href && currentPath && href.endsWith(currentPath))) {
console.log('Matched link:', href, 'with current path:', currentPath);
link.classList.add('bg-indigo-50', 'text-indigo-700', 'font-semibold');
link.classList.remove('text-gray-700');
if (icon) {
icon.classList.remove('text-gray-400', 'group-hover:text-gray-600');
icon.classList.add('text-indigo-600');
}
}
});
}
// Restore on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
restoreSidebarScroll();
highlightCurrentDocument();
});
} else {
restoreSidebarScroll();
highlightCurrentDocument();
}
// Also restore on window load (for safety)
window.addEventListener('load', () => {
restoreSidebarScroll();
highlightCurrentDocument();
});
// Update highlight after Alpine navigation
document.addEventListener('alpine:navigated', highlightCurrentDocument);
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault(); e.preventDefault();

View File

@@ -1,4 +1,4 @@
<div class="max-w-5xl mx-auto p-8"> <div class="max-w-5xl mx-auto p-4 sm:p-6 lg:p-8">
<!-- Flash Messages --> <!-- Flash Messages -->
@if (session()->has('message')) @if (session()->has('message'))
<div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded"> <div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded">
@@ -13,16 +13,16 @@
@endif @endif
<!-- Header --> <!-- Header -->
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-3xl font-bold text-gray-900"> <h1 class="text-2xl sm:text-3xl font-bold text-gray-900">
{{ $isEditMode ? __('messages.documents.edit_document') : __('messages.documents.new_document') }} {{ $isEditMode ? __('messages.documents.edit_document') : __('messages.documents.new_document') }}
</h1> </h1>
<div class="flex space-x-3"> <div class="flex flex-wrap gap-2 sm:gap-3">
@if($isEditMode && $document) @if($isEditMode && $document)
<a <a
href="{{ route('documents.show', $document) }}" href="{{ route('documents.show', $document) }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex-1 sm:flex-none"
> >
{{ __('messages.common.cancel') }} {{ __('messages.common.cancel') }}
</a> </a>
@@ -30,14 +30,14 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text
<button <button
wire:click="delete" wire:click="delete"
wire:confirm="{{ __('messages.documents.delete_confirm') }}" wire:confirm="{{ __('messages.documents.delete_confirm') }}"
class="inline-flex items-center px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50 flex-1 sm:flex-none"
> >
{{ __('messages.documents.delete') }} {{ __('messages.documents.delete') }}
</button> </button>
@else @else
<a <a
href="{{ route('documents.show', 'home') }}" href="{{ route('documents.show', 'home') }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex-1 sm:flex-none"
> >
{{ __('messages.common.cancel') }} {{ __('messages.common.cancel') }}
</a> </a>
@@ -45,12 +45,13 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text
<button <button
wire:click="save" wire:click="save"
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700 flex-1 sm:flex-none"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
</svg> </svg>
{{ __('messages.documents.save') }} <span class="hidden sm:inline">{{ __('messages.documents.save') }}</span>
<span class="sm:hidden">{{ __('messages.documents.save') }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,15 @@
<div class="max-w-4xl mx-auto p-8"> <div class="max-w-4xl mx-auto p-4 sm:p-6 lg:p-8">
<!-- Document Header --> <!-- Document Header -->
<div class="mb-8"> <div class="mb-6 sm:mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-4">
<h1 class="text-4xl font-bold text-gray-900"> <h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 break-words">
{{ $document->title }} {{ $document->title }}
</h1> </h1>
@auth @auth
<a <a
href="{{ route('documents.edit', $document) }}" href="{{ route('documents.edit', $document) }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 whitespace-nowrap"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
@@ -19,9 +19,9 @@ class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-
@endauth @endauth
</div> </div>
<div class="flex items-center text-sm text-gray-500 space-x-4"> <div class="flex flex-col sm:flex-row sm:items-center text-xs sm:text-sm text-gray-500 gap-2 sm:gap-4">
<span> <span>
{{ __('messages.documents.updated') }} {{ $document->updated_at->diffForHumans() }} {{ __('messages.documents.updated') }} {{ $document->updated_at->diffForHumans() }}{{ config('app.timezone') }}
</span> </span>
@if($document->updated_by && $document->updater) @if($document->updated_by && $document->updater)
@@ -37,7 +37,7 @@ class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-
</div> </div>
<!-- Document Content --> <!-- Document Content -->
<div class="prose prose-lg max-w-none mb-12"> <div class="prose prose-sm sm:prose-base lg:prose-lg max-w-none mb-8 sm:mb-12">
{!! $renderedContent !!} {!! $renderedContent !!}
</div> </div>
@@ -69,15 +69,15 @@ class="block p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition"
@endif @endif
<!-- Document Metadata --> <!-- Document Metadata -->
<div class="mt-12 pt-8 border-t border-gray-200"> <div class="mt-8 sm:mt-12 pt-6 sm:pt-8 border-t border-gray-200">
<div class="grid grid-cols-2 gap-4 text-sm text-gray-500"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs sm:text-sm text-gray-500">
<div> <div class="break-all">
<span class="font-medium">{{ __('messages.documents.path') }}:</span> <span class="font-medium">{{ __('messages.documents.path') }}:</span>
<code class="ml-2 text-xs bg-gray-100 px-2 py-1 rounded">{{ $document->path }}</code> <code class="ml-2 text-xs bg-gray-100 px-2 py-1 rounded">{{ $document->path }}</code>
</div> </div>
<div> <div>
<span class="font-medium">{{ __('messages.documents.last_modified') }}:</span> <span class="font-medium">{{ __('messages.documents.last_modified') }}:</span>
<span class="ml-2">{{ $document->updated_at->format('Y-m-d H:i:s') }}</span> <span class="ml-2">{{ $document->updated_at->format('Y-m-d H:i:s') }}{{ config('app.timezone') }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@ class="fixed inset-0 z-50 overflow-y-auto"
style="display: none;" style="display: none;"
@click="open = false" @click="open = false"
> >
<div class="flex min-h-full items-start justify-center p-4 pt-[10vh]"> <div class="flex min-h-full items-start justify-center p-2 sm:p-4 pt-[5vh] sm:pt-[10vh]">
<div <div
class="w-full max-w-2xl bg-white rounded-lg shadow-2xl" class="w-full max-w-2xl bg-white rounded-lg shadow-2xl"
@click.stop @click.stop
@@ -41,16 +41,16 @@ class="w-full max-w-2xl bg-white rounded-lg shadow-2xl"
wire:keydown.enter.prevent="selectDocument" wire:keydown.enter.prevent="selectDocument"
> >
<!-- Search Input --> <!-- Search Input -->
<div class="p-4 border-b border-gray-200"> <div class="p-3 sm:p-4 border-b border-gray-200">
<div class="relative"> <div class="relative">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="absolute left-2 sm:left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 sm:h-5 sm:w-5 text-gray-400" 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> <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> </svg>
<input <input
x-ref="searchInput" x-ref="searchInput"
type="text" type="text"
wire:model.live="search" wire:model.live="search"
class="w-full pl-10 pr-4 py-3 border-0 focus:ring-0 text-lg" class="w-full pl-8 sm:pl-10 pr-4 py-2 sm:py-3 border-0 focus:ring-0 text-base sm:text-lg"
placeholder="{{ __('messages.quick_switcher.placeholder') }}" placeholder="{{ __('messages.quick_switcher.placeholder') }}"
autocomplete="off" autocomplete="off"
> >
@@ -58,7 +58,7 @@ class="w-full pl-10 pr-4 py-3 border-0 focus:ring-0 text-lg"
</div> </div>
<!-- Results --> <!-- Results -->
<div class="max-h-96 overflow-y-auto"> <div class="max-h-60 sm:max-h-96 overflow-y-auto">
@if(empty($this->results)) @if(empty($this->results))
<div class="p-8 text-center text-gray-500"> <div class="p-8 text-center text-gray-500">
{{ __('messages.quick_switcher.no_results') }} {{ __('messages.quick_switcher.no_results') }}
@@ -102,21 +102,21 @@ class="block px-4 py-3 hover:bg-gray-50 transition {{ $index === $selectedIndex
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="px-4 py-3 bg-gray-50 border-t border-gray-200 text-xs text-gray-500"> <div class="px-3 sm:px-4 py-2 sm:py-3 bg-gray-50 border-t border-gray-200 text-xs text-gray-500">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-2 sm:space-x-4 flex-wrap gap-y-1">
<span class="flex items-center"> <span class="flex items-center">
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1"></kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1"></kbd>
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-2"></kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1 sm:mr-2"></kbd>
{{ __('messages.quick_switcher.navigate') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.navigate') }}</span>
</span> </span>
<span class="flex items-center"> <span class="flex items-center">
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-2"></kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1 sm:mr-2"></kbd>
{{ __('messages.quick_switcher.select') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.select') }}</span>
</span> </span>
<span class="flex items-center"> <span class="flex items-center">
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-2">esc</kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1 sm:mr-2">esc</kbd>
{{ __('messages.quick_switcher.close') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.close') }}</span>
</span> </span>
</div> </div>
</div> </div>