diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php new file mode 100644 index 0000000..b04ada5 --- /dev/null +++ b/app/Http/Controllers/Settings/PasswordController.php @@ -0,0 +1,32 @@ +validateWithBag('updatePassword', [ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('status', 'password-updated'); + } +} diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php new file mode 100644 index 0000000..e31202c --- /dev/null +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -0,0 +1,57 @@ + $request->user(), + 'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail, + 'status' => session('status'), + ]); + } + + public function update(Request $request): RedirectResponse + { + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', + 'unique:users,email,' . $request->user()->id], + ]); + + $request->user()->fill($data); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return back()->with('status', 'profile-updated'); + } + + public function destroy(Request $request): RedirectResponse + { + $request->validateWithBag('userDeletion', [ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + Auth::logout(); + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Livewire/FileManager.php b/app/Livewire/FileManager.php index b8d70d1..ff06109 100644 --- a/app/Livewire/FileManager.php +++ b/app/Livewire/FileManager.php @@ -167,6 +167,7 @@ public function saveFile(string $content): void $this->fileContent = $content; $this->isDirty = false; + $this->dispatch('file-saved'); // Alpine resets dirty indicator $this->showFlash('Saved ✓'); } diff --git a/resources/css/app.css b/resources/css/app.css index 5b11777..9e3cae7 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -11,6 +11,8 @@ @theme { --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + + /* ── Zinc scale ───────────────────────────────────────────────────── */ --color-zinc-50: #fafafa; --color-zinc-100: #f5f5f5; --color-zinc-200: #e5e5e5; @@ -22,16 +24,31 @@ --color-zinc-800: #262626; --color-zinc-900: #171717; --color-zinc-950: #0a0a0a; - --color-accent: var(--color-neutral-800); - --color-accent-content: var(--color-neutral-800); + + /* ── Brand color (indigo-based, customize here) ───────────────────── */ + --color-brand-50: #eef2ff; + --color-brand-100: #e0e7ff; + --color-brand-200: #c7d2fe; + --color-brand-300: #a5b4fc; + --color-brand-400: #818cf8; + --color-brand-500: #6366f1; + --color-brand-600: #4f46e5; + --color-brand-700: #4338ca; + --color-brand-800: #3730a3; + --color-brand-900: #312e81; + --color-brand-950: #1e1b4b; + + /* ── Flux accent (maps to brand) ─────────────────────────────────── */ + --color-accent: var(--color-brand-500); + --color-accent-content: var(--color-brand-500); --color-accent-foreground: var(--color-white); } @layer theme { .dark { - --color-accent: var(--color-white); - --color-accent-content: var(--color-white); - --color-accent-foreground: var(--color-neutral-800); + --color-accent: var(--color-brand-400); + --color-accent-content: var(--color-brand-400); + --color-accent-foreground: var(--color-zinc-950); } } @@ -43,8 +60,19 @@ ::file-selector-button { border-color: var(--color-gray-200, currentColor); } + + /* Smooth theme transition */ + html { + color-scheme: light dark; + } + + html.transitioning, + html.transitioning * { + transition: background-color 0.2s ease, color 0.15s ease, border-color 0.15s ease !important; + } } +/* ── Flux field/label overrides ─────────────────────────────────────────── */ [data-flux-field]:not(ui-radio, ui-checkbox) { @apply grid gap-2; } @@ -59,7 +87,7 @@ select:focus[data-flux-control] { @apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground; } -/* ── CodeMirror 6 ─────────────────────────────────────────────────────────── */ +/* ── CodeMirror 6 ───────────────────────────────────────────────────────── */ .cm-editor { height: 100%; font-size: 0.85rem; @@ -67,5 +95,5 @@ select:focus[data-flux-control] { .cm-scroller { overflow: auto; - font-family: ui-monospace, 'Cascadia Code', 'JetBrains Mono', monospace; + font-family: ui-monospace, 'Cascadia Code', 'JetBrains Mono', Menlo, monospace; } diff --git a/resources/js/file-manager.js b/resources/js/file-manager.js index 6ad36a4..8482604 100644 --- a/resources/js/file-manager.js +++ b/resources/js/file-manager.js @@ -104,7 +104,9 @@ function createEditor(content, lang) { EditorView.updateListener.of(update => { if (update.selectionSet) publishCursor(update.state); if (!update.docChanged) return; - getWire()?.set('isDirty', true); + // Dispatch to Alpine only — never call $wire.set() here. + // $wire.set() triggers a full Livewire re-render which destroys #cm-host. + window.dispatchEvent(new CustomEvent('cm-dirty')); clearTimeout(saveDebounce); saveDebounce = setTimeout(triggerSave, 1200); }), @@ -135,6 +137,9 @@ document.addEventListener('alpine:init', () => { // Drag-and-drop state dragOver: false, + // Dirty state — tracked purely in Alpine, never via $wire.set() + dirty: false, + // ── Modal helpers ──────────────────────────────────────────────── openModal(name) { this.modal = name; }, closeModal() { this.modal = null; }, @@ -168,12 +173,21 @@ document.addEventListener('alpine:init', () => { // ── Init ───────────────────────────────────────────────────────── init() { - // CodeMirror events + // CodeMirror cursor position window.addEventListener('cm-cursor', e => { this.cursor = e.detail; }); + + // Editor content changed — mark dirty in Alpine only (never via $wire.set) + window.addEventListener('cm-dirty', () => { this.dirty = true; }); + + // File opened — reset dirty and create editor window.addEventListener('load-editor', e => { + this.dirty = false; createEditor(e.detail.content, e.detail.language); }); + // Livewire saved successfully — reset dirty + window.addEventListener('file-saved', () => { this.dirty = false; }); + // Livewire → Alpine modal control window.addEventListener('open-modal', e => this.openModal(e.detail?.name)); window.addEventListener('close-modal', () => this.closeModal()); diff --git a/resources/views/.DS_Store b/resources/views/.DS_Store deleted file mode 100644 index 0a1117d..0000000 Binary files a/resources/views/.DS_Store and /dev/null differ diff --git a/resources/views/components/app-layout.blade.php b/resources/views/components/app-layout.blade.php index eb49845..32e8641 100644 --- a/resources/views/components/app-layout.blade.php +++ b/resources/views/components/app-layout.blade.php @@ -7,64 +7,90 @@