diff --git a/app/Livewire/FileManager.php b/app/Livewire/FileManager.php index 81ff2e6..b8d70d1 100644 --- a/app/Livewire/FileManager.php +++ b/app/Livewire/FileManager.php @@ -3,7 +3,6 @@ namespace App\Livewire; use App\Models\Page; -use Flux\Flux; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Storage; use Livewire\Attributes\Computed; @@ -210,7 +209,7 @@ public function createFileAndClose(): void $this->disk()->put($full, ''); $this->newFileName = ''; - Flux::modal('new-file')->close(); + $this->dispatch('close-modal'); $this->openFile($relative); unset($this->fileTree); } @@ -238,7 +237,7 @@ public function createFolderAndClose(): void $this->disk()->makeDirectory($this->page->storagePath($relative)); $this->newFolderName = ''; - Flux::modal('new-folder')->close(); + $this->dispatch('close-modal'); $this->showFlash("Folder '{$name}' created."); unset($this->fileTree); } @@ -250,7 +249,7 @@ public function prepareDelete(string $path, string $type = 'file'): void $this->deleteTarget = $path; $this->deleteTargetName = basename($path); $this->deleteTargetType = $type; - Flux::modal('delete-confirm')->show(); + $this->dispatch('open-modal', name: 'delete-confirm'); } /** Execute the pending delete after modal confirmation. */ @@ -275,7 +274,7 @@ public function executeDelete(): void $this->showFlash("Deleted {$this->deleteTargetName}."); $this->deleteTarget = ''; $this->deleteTargetName = ''; - Flux::modal('delete-confirm')->close(); + $this->dispatch('close-modal'); unset($this->fileTree); } @@ -339,7 +338,7 @@ public function uploadFile(): void $this->upload = null; $this->showFlash("Uploaded {$name}."); - Flux::modal('upload')->close(); + $this->dispatch('close-modal'); unset($this->fileTree); } @@ -360,7 +359,7 @@ public function saveSettings(): void ]); $this->page->refresh(); - Flux::modal('settings')->close(); + $this->dispatch('close-modal'); $this->showFlash('Settings saved.'); if ($this->page->wasChanged('slug')) { diff --git a/resources/js/file-manager.js b/resources/js/file-manager.js index 96a70a3..6ad36a4 100644 --- a/resources/js/file-manager.js +++ b/resources/js/file-manager.js @@ -1,14 +1,14 @@ /** - * file-manager.js — loaded only on the file manager page. + * file-manager.js — loaded only on the file manager page. * - * Alpine is provided by @fluxScripts (bundled with Livewire/Flux). - * We must NOT import or start our own Alpine — Flux registers its modal - * plugin on its own Alpine instance. We hook into it via alpine:init. + * Alpine is provided by @livewireScripts (Livewire 4 bundles Alpine 3). + * We register our data component via alpine:init — that hooks into + * Livewire's Alpine instance after it is created but before DOM init. * - * CodeMirror 6 is tree-shaken and bundled here by Vite. + * Modals are plain Alpine x-show panels — no Flux Pro required. */ -import { EditorState, Compartment } from '@codemirror/state'; +import { EditorState, Compartment } from '@codemirror/state'; import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine, @@ -22,14 +22,14 @@ import { indentOnInput, bracketMatching, foldGutter, syntaxHighlighting, defaultHighlightStyle, } from '@codemirror/language'; -import { html } from '@codemirror/lang-html'; -import { css } from '@codemirror/lang-css'; -import { javascript } from '@codemirror/lang-javascript'; -import { json } from '@codemirror/lang-json'; -import { xml } from '@codemirror/lang-xml'; -import { oneDark } from '@codemirror/theme-one-dark'; +import { html } from '@codemirror/lang-html'; +import { css } from '@codemirror/lang-css'; +import { javascript } from '@codemirror/lang-javascript'; +import { json } from '@codemirror/lang-json'; +import { xml } from '@codemirror/lang-xml'; +import { oneDark } from '@codemirror/theme-one-dark'; -// ── CodeMirror state ────────────────────────────────────────────────────────── +// ── CodeMirror ──────────────────────────────────────────────────────────────── const langCompartment = new Compartment(); let editorView = null; @@ -39,7 +39,7 @@ function publishCursor(state) { const head = state.selection.main.head; const line = state.doc.lineAt(head); window.dispatchEvent(new CustomEvent('cm-cursor', { - detail: { line: line.number, col: head - line.from + 1, total: state.doc.lines }, + detail: { line: line.number, col: head - line.from + 1 }, })); } @@ -64,10 +64,9 @@ function triggerSave() { getWire()?.call('saveFile', editorView.state.doc.toString()); } -export function createEditor(content, lang) { +function createEditor(content, lang) { const host = document.getElementById('cm-host'); if (!host) return; - editorView?.destroy(); editorView = null; @@ -78,7 +77,7 @@ export function createEditor(content, lang) { oneDark, EditorView.theme({ '&': { height: '100%' }, - '.cm-scroller': { overflow: 'auto', fontFamily: "ui-monospace, 'Cascadia Code', 'JetBrains Mono', Menlo, monospace" }, + '.cm-scroller': { overflow: 'auto', fontFamily: "ui-monospace,'Cascadia Code','JetBrains Mono',Menlo,monospace" }, '.cm-content': { padding: '8px 0' }, '.cm-gutters': { background: 'transparent', border: 'none' }, }), @@ -117,45 +116,72 @@ export function createEditor(content, lang) { publishCursor(editorView.state); } -// ── Alpine component — registered on Flux/Livewire's Alpine instance ────────── -// -// We use alpine:init so our data component is registered BEFORE Alpine -// processes the DOM, but AFTER Flux has registered its own plugins -// (including x-flux-modal and $dispatch helpers). +// ── Alpine component ────────────────────────────────────────────────────────── document.addEventListener('alpine:init', () => { - // Safety guard: window.Alpine is set by @fluxScripts before this fires. - if (!window.Alpine) return; - window.Alpine.data('fileManager', () => ({ + // Modal state — name of currently open modal, or null + modal: null, + + // File tree — which directories are expanded openDirs: [], + + // Context-menu state ctx: { show: false, x: 0, y: 0, item: null }, + + // Status-bar cursor info cursor: { line: 1, col: 1 }, - isOpen(path) { return this.openDirs.includes(path); }, - toggleDir(path) { + // Drag-and-drop state + dragOver: false, + + // ── Modal helpers ──────────────────────────────────────────────── + openModal(name) { this.modal = name; }, + closeModal() { this.modal = null; }, + isModal(name) { return this.modal === name; }, + + // ── File-tree helpers ──────────────────────────────────────────── + isOpen(path) { return this.openDirs.includes(path); }, + toggleDir(path) { this.openDirs = this.isOpen(path) ? this.openDirs.filter(p => p !== path) : [...this.openDirs, path]; }, - openContext(e, item) { + // ── Context menu ───────────────────────────────────────────────── + openCtx(e, item) { + e.preventDefault(); this.ctx = { show: true, x: e.clientX, y: e.clientY, item }; }, - closeContext() { - this.ctx = { show: false, x: 0, y: 0, item: null }; + closeCtx() { this.ctx.show = false; }, + + // ── Drag-and-drop upload ───────────────────────────────────────── + onDrop(e) { + this.dragOver = false; + const files = Array.from(e.dataTransfer?.files ?? []); + if (!files.length) return; + files.forEach(file => { + const wire = getWire(); + if (wire) wire.upload('upload', file, () => {}, () => {}, () => {}); + }); }, + // ── Init ───────────────────────────────────────────────────────── init() { + // CodeMirror events window.addEventListener('cm-cursor', e => { this.cursor = e.detail; }); - window.addEventListener('load-editor', e => { createEditor(e.detail.content, e.detail.language); }); - window.addEventListener('click', () => this.closeContext()); + // Livewire → Alpine modal control + window.addEventListener('open-modal', e => this.openModal(e.detail?.name)); + window.addEventListener('close-modal', () => this.closeModal()); + + // Close context menu on click/Escape + window.addEventListener('click', () => this.closeCtx()); window.addEventListener('keydown', e => { - if (e.key === 'Escape') this.closeContext(); + if (e.key === 'Escape') { this.closeCtx(); this.closeModal(); } }); }, diff --git a/resources/views/.DS_Store b/resources/views/.DS_Store new file mode 100644 index 0000000..0a1117d Binary files /dev/null and b/resources/views/.DS_Store differ diff --git a/resources/views/livewire/file-manager.blade.php b/resources/views/livewire/file-manager.blade.php index 8c247e2..df45709 100644 --- a/resources/views/livewire/file-manager.blade.php +++ b/resources/views/livewire/file-manager.blade.php @@ -1,520 +1,460 @@ {{-- - File Manager — full-screen IDE layout. - - Alpine root: fileManager() from resources/js/file-manager.js - Livewire: App\Livewire\FileManager - - Conventions: - - x-on: prefix everywhere (avoids @livewire Blade directive collision) - - Flux modals for new-file, new-folder, rename, delete-confirm, settings - - Context menu fully in Alpine + File Manager + - Modals: pure Alpine x-show (no Flux Pro required) + - Flux used only for free components: icons, inputs, buttons, badges + - Drag-and-drop upload --}} -
- {{-- ═══════════════════════════════════════════════════════════════════════ - TOP BAR - ═══════════════════════════════════════════════════════════════════════ --}} -
- - {{-- Back --}} - - - - - -
- - {{-- Page info --}} -
- {{ $page->name }} - - {{ $page->slug }} - - @if ($page->is_public) - - @else - - @endif -
- -
- - {{-- Unsaved indicator --}} - @if ($isDirty) - - - Unsaved +{{-- ══════════════════════════════════════════════════════════════════════ + TOP BAR +══════════════════════════════════════════════════════════════════════ --}} +
+ + + Dashboard + + +
+ +
+ {{ $page->name }} + + {{ $page->slug }} + + @if ($page->is_public) + + @else + @endif +
- {{-- Preview --}} - - - {{-- Open in new tab --}} - - - {{-- Settings --}} - +
+ +{{-- ══════════════════════════════════════════════════════════════════════ + MAIN SPLIT +══════════════════════════════════════════════════════════════════════ --}} +
+ +{{-- ── SIDEBAR ──────────────────────────────────────────────────────── --}} +
- - - {{-- ═══════════════════════════════════════════════════════════════════════ - MAIN SPLIT PANE - ═══════════════════════════════════════════════════════════════════════ --}} -
- - {{-- ── LEFT SIDEBAR ──────────────────────────────────────────────── --}} -
- {{-- Sidebar toolbar --}} -
- - Explorer - - - - - + / {{ $currentDir }} +
+ @endif + + {{-- File tree --}} +
+ @forelse ($this->fileTree as $item) + @include('livewire.partials.tree-item', ['item' => $item, 'depth' => 0]) + @empty +
+ +

No files yet

+
+ @endforelse +
- {{-- Current directory breadcrumb --}} - @if ($currentDir) -
- + +
+
+ @endif + + {{-- Drop-zone hint --}} +
+ + Drop to upload +
+ + +{{-- ── EDITOR PANE ──────────────────────────────────────────────────── --}} +
+ + {{-- Editor toolbar --}} +
+ + + {{ $activeFile ?: 'No file open' }} + +
+ @if ($isDirty) + - - / {{ $currentDir }} + @elseif ($activeFile && $this->isEditable) + + Saved -
- @endif - - {{-- File tree --}} -
- @forelse ($this->fileTree as $item) - @include('livewire.partials.tree-item', ['item' => $item, 'depth' => 0]) - @empty -
- -

No files yet

- -
- @endforelse -
- - {{-- Rename inline form (shown when renameTarget is set) --}} - @if ($renameTarget !== null) -
-

- Rename -

- -
- - -
-
@endif - - - - - {{-- ── CENTER EDITOR PANE ──────────────────────────────────────── --}} -
- - {{-- Editor toolbar --}} -
- @if ($activeFile) - - - {{ $activeFile }} - - @else - No file open - @endif - -
- @if ($isDirty) - - @elseif ($activeFile && $this->isEditable) - - - Saved - - @endif - - @if ($activeFile && $this->isPreviewable && !$this->isEditable) - - - Open - - @endif -
-
- - {{-- ── Content area ── --}} - - @if ($this->isEditable && $activeFile) - {{-- CodeMirror host --}} -
- - @elseif ($this->isPreviewable && $activeFile) - {{-- Media / binary preview --}} - @php $ext = strtolower(pathinfo($activeFile, PATHINFO_EXTENSION)); @endphp -
- @if (in_array($ext, ['png','jpg','jpeg','gif','webp','ico'])) - - @elseif ($ext === 'svg') - - @elseif (in_array($ext, ['mp4','webm'])) - - @elseif (in_array($ext, ['mp3','wav','ogg'])) -
- -

{{ basename($activeFile) }}

- -
- @elseif ($ext === 'pdf') - - @endif -
- - @elseif ($activeFile) - {{-- Non-previewable binary --}} -
- -

{{ basename($activeFile) }}

-

Binary file — cannot be edited in browser

-
- - @else - {{-- Empty state --}} -
- -

No file open

-

Select a file from the explorer to start editing

- -
- @endif -
-
- - {{-- ═══════════════════════════════════════════════════════════════════════ - STATUS BAR - ═══════════════════════════════════════════════════════════════════════ --}} - - - - {{-- ═══════════════════════════════════════════════════════════════════════ - CONTEXT MENU (Alpine, fully client-side) - ═══════════════════════════════════════════════════════════════════════ --}} - - - - {{-- ═══════════════════════════════════════════════════════════════════════ - FLASH TOAST - ═══════════════════════════════════════════════════════════════════════ --}} - +
+ +{{-- ══════════════════════════════════════════════════════════════════════ + STATUS BAR +══════════════════════════════════════════════════════════════════════ --}} + + +{{-- ══════════════════════════════════════════════════════════════════════ + CONTEXT MENU +══════════════════════════════════════════════════════════════════════ --}} + + +{{-- ══════════════════════════════════════════════════════════════════════ + FLASH TOAST +══════════════════════════════════════════════════════════════════════ --}} + - {{-- ═══════════════════════════════════════════════════════════════════════ - MODALS - ═══════════════════════════════════════════════════════════════════════ --}} +{{-- ══════════════════════════════════════════════════════════════════════ + MODALS (pure Alpine x-show — no Flux Pro required) +══════════════════════════════════════════════════════════════════════ --}} + +{{-- Shared backdrop --}} + - {{-- ── New File ────────────────────────────────────────────────────── --}} - -
- New File - - Create a new file in - - {{ $currentDir ?: '/' }} - - - - - File name - - @error('newFileName') {{ $message }} @enderror - - -
- +{{-- ── Modal: New File ─────────────────────────────────────────────── --}} + - {{-- ── New Folder ──────────────────────────────────────────────────── --}} - -
- New Folder - Create a folder inside - - {{ $currentDir ?: '/' }} - - - - - Folder name - - @error('newFolderName') {{ $message }} @enderror - - -
- +{{-- ── Modal: New Folder ────────────────────────────────────────────── --}} + - {{-- ── Upload ──────────────────────────────────────────────────────── --}} - -
- Upload Files - - Upload to {{ $currentDir ?: '/' }} - — max {{ config('filesystems.max_upload_mb', 50) }} MB per file - - -