From 4bdcd7fb761b8257aa9702c8c8434766f00a3492 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 22:11:20 +0200 Subject: [PATCH 01/33] Create a new dashboard design --- resources/dashboard.blade.php | 1454 +++++++++++++++++++++++++-------- 1 file changed, 1134 insertions(+), 320 deletions(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index de3f37a..c76f4f0 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -5,91 +5,888 @@ - {{ $title }} - - - -
-
-
-

{{ $title }}

-
-

Welcome to the dashboard for your HydePHP site.

-

This page is accessible through the Hyde Realtime Compiler and will not be saved to your static site.

-
-
+
+ +
+
+

Dashboard

+

This page is served by the Realtime Compiler and won't be saved to your static site.

-
-
-
-
-
-

Project Information

- @if($dashboard->isInteractive()) -
- - - -
- @endif -
-
-
- - - @foreach($dashboard->getProjectInformation() as $type => $info) - - @endforeach - -
- {{ $type }}: - {{ $info }} - @if($loop->last) - + + @endif + +
+
+ @php $swatches = ['blue', 'purple', 'teal']; $i = 0; @endphp + @foreach($dashboard->getProjectInformation() as $type => $info) + @php $swatch = $swatches[$i % count($swatches)]; $i++; @endphp +
+ + @if($loop->last) + + @elseif($loop->first) + + @else + + @endif + +
+
{{ $type }}
+
+ @if($loop->last) +
+ {{ $info }} + - @endif -
-
+
+ @else + {{ $info }} + @endif +
+ + + @endforeach
-
-
-
-
-
-

Site Pages & Routes

- @if($dashboard->isInteractive()) - - - + + +
+ + + + - -
+ @endif +
+
+ @if(empty($dashboard->getPageList())) +
+ No pages yet + There are no pages yet. @if($dashboard->isInteractive()) Create one using the button above. @else Why not create some? @endif
-
+ @else +
+ + + + + + + + + + + + @php + $typeColors = [ + 'BladePage' => 'purple', + 'MarkdownPage' => 'blue', + 'MarkdownPost' => 'teal', + 'DocumentationPage' => 'amber', + ]; + @endphp + @foreach($dashboard->getPageList() as $route) + @php + $typeKey = class_basename($route->getPageClass()); + $color = $typeColors[$typeKey] ?? 'blue'; + @endphp + getFlash('justCreatedPage') === $route->getRouteKey() ? 'justCreatedPage just-created' : ''])> + + + + + + + @endforeach + +
Page typeRoute keySource fileOutput file
+ + {{ $typeKey }} + + {{ $route->getRouteKey() }} + @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) + <none> + @else + {{ $route->getSourcePath() }} + @endif + {{ $route->getOutputPath() }} +
+ @if($dashboard->isInteractive()) +
+ + + + @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) + + @else + + @endif +
+ @endif + View +
+
+
+ @endif
-
-
-
-
-
-

Media Library

-
+ + {{-- Media library --}} +
+
+

Media library

+
+
+ @if(empty(\Hyde\Support\Filesystem\MediaFile::all())) +
+ No media files yet + Why not add some?
-
- @if(empty(\Hyde\Support\Filesystem\MediaFile::all())) -
- Info: - There are no media files yet. Why not add some? -
- @else -
- @foreach(\Hyde\Support\Filesystem\MediaFile::all() as $mediaFile) -
-
- @if(in_array($mediaFile->getExtension(), ['svg', 'png', 'jpg', 'jpeg', 'gif', 'ico'])) - {{ $mediaFile->getName() }} + @else +
+ @foreach(\Hyde\Support\Filesystem\MediaFile::all() as $mediaFile) +
+
+ @if(in_array($mediaFile->getExtension(), ['svg', 'png', 'jpg', 'jpeg', 'gif', 'ico'])) + {{ $mediaFile->getName() }} + @else +
+
+ @endif +
+ Fullscreen + @if($dashboard->isInteractive()) +
+ + + + +
+ @endif +
+
+
+ {{ $mediaFile->getExtension() }} +
+
{{ $mediaFile->getName() }}
+
{{ $dashboard::bytesToHuman($mediaFile->getLength()) }}
- @endforeach +
- @endif + @endforeach
-
+ @endif
+ @if($dashboard->showTips()) -
-
-
- Tip: - {{ $dashboard->getTip() }} -
-
-
+
+ + + +

Tip: {{ $dashboard->getTip() }}

+
@endif
-
-
-
-
-

- HydePHP Realtime Compiler {{ $dashboard->getVersion() }} -

-
-
-
+ +
+

HydePHP Realtime Compiler {{ $dashboard->getVersion() }}

+ @if($dashboard->isInteractive()) {{-- Interactivity is not needed when editor is disabled --}} - - - -
- @@ -1142,66 +1140,224 @@

HydePHP Realtime Compiler {{ $dashboard->getVersion() }}

-@if($dashboard->isInteractive()) - {{-- Interactivity is not needed when editor is disabled --}} +{{-- Quick view is read-only, so it's available even when the dashboard is not interactive --}} + + + + +@if($dashboard->isInteractive())
+@endif - - -@endif + document.querySelectorAll('[data-dialog-close]').forEach(button => { + button.addEventListener('click', function () { + this.closest('dialog')?.close(); + }); + }); + + /* ---------- quick view ---------- */ + + document.querySelectorAll('.quick-view-btn').forEach(button => { + button.addEventListener('click', function () { + document.getElementById('quickViewModalLabel').innerText = this.dataset.previewLabel; + document.getElementById('quickViewOpenLink').href = this.dataset.previewUrl; + document.getElementById('quickViewFrame').src = this.dataset.previewUrl; + document.getElementById('quickViewModal').showModal(); + }); + }); + + document.getElementById('quickViewModal')?.addEventListener('close', () => { + document.getElementById('quickViewFrame').src = 'about:blank'; + }); + + @if($dashboard->isInteractive()) + /* ---------- error toast ---------- */ + + const toast = document.getElementById('asyncErrorToast'); + + function showErrorToast(title, message) { + document.getElementById('asyncErrorToastHeader').innerText = title; + document.getElementById('asyncErrorToastBody').innerText = message; + toast.classList.add('show'); + clearTimeout(toast._hideTimer); + toast._hideTimer = setTimeout(() => toast.classList.remove('show'), 6000); + } + + document.querySelector('[data-toast-close]')?.addEventListener('click', () => { + toast.classList.remove('show'); + }); + + /* ---------- create page modal open ---------- */ + + document.getElementById('createPageModalButton')?.addEventListener('click', () => { + const modal = document.getElementById('createPageModal'); + modal.showModal(); + document.getElementById('pageTypeSelection').focus(); + }); + + /* ---------- async forms ---------- */ + + function registerAsyncForm(form, okHandler = null, errorHandler = null, beforeCallHandler = null, afterCallHandler = null) { + form.addEventListener('submit', function (event) { + event.preventDefault(); + + beforeCallHandler?.(); + + fetch('', { + method: 'POST', + body: new FormData(event.target), + headers: new Headers({'Accept': 'application/json'}), + }).then(async response => { + if (response.ok) { + okHandler?.(response); + } else if (errorHandler) { + errorHandler(response); + } else { + const data = await response.json(); + showErrorToast(`Error: ${response.status} ${response.statusText}`, data.error); + } + }).catch(error => { + console.error('Network error:', error); + }); + + afterCallHandler?.(); + }); + } + + document.querySelectorAll('.buttonActionForm').forEach(form => registerAsyncForm(form)); + + /* ---------- create page form ---------- */ + + const createPageForm = document.getElementById('createPageForm'); + + if (createPageForm) { + const createPageButton = document.getElementById('createPageButton'); + const createPageFormError = document.getElementById('createPageFormError'); + const createPageFormErrorContents = document.getElementById('createPageFormErrorContents'); + + registerAsyncForm(createPageForm, () => { + document.getElementById('createPageModal').close(); + createPageForm.reset(); + location.reload(); + }, async response => { + const data = await response.json(); + createPageFormError.style.display = 'block'; + createPageFormErrorContents.innerText = data.error; + }, () => { + createPageButton.disabled = true; + createPageFormError.style.display = 'none'; + createPageFormErrorContents.innerText = ''; + }, () => { + createPageButton.disabled = false; + }); + + /* ---------- page type field switching ---------- */ + + const createPageModalLabel = document.getElementById('createPageModalLabel'); + const titleInputLabel = document.getElementById('titleInputLabel'); + const contentInputLabel = document.getElementById('contentInputLabel'); + const contentInput = document.getElementById('contentInput'); + const pageTypeSelection = document.getElementById('pageTypeSelection'); + const baseInfo = document.getElementById('baseInfo'); + const createsPost = document.getElementById('createsPost'); + + const defaults = { + modalLabel: createPageModalLabel.innerText, + titleLabel: titleInputLabel.innerText, + contentLabel: contentInputLabel.innerText, + contentPlaceholder: contentInput.placeholder, + }; + + pageTypeSelection.addEventListener('change', function (event) { + createPageModalLabel.innerText = defaults.modalLabel; + titleInputLabel.innerText = defaults.titleLabel; + contentInputLabel.innerText = defaults.contentLabel; + contentInput.placeholder = defaults.contentPlaceholder; + + createPageButton.disabled = false; + createPageButton.title = ''; + + baseInfo.style.display = 'none'; + createsPost.style.display = 'none'; + + const selection = event.target.value; + + if (selection === 'markdown-post') { + baseInfo.style.display = 'block'; + createsPost.style.display = 'block'; + createPageModalLabel.innerText = 'Creating a new Markdown post'; + titleInputLabel.innerText = 'Post title'; + } else { + baseInfo.style.display = 'block'; + createPageModalLabel.innerText = 'Creating a new ' + toTitleCase(selection); + } + + if (selection === 'blade-page') { + contentInputLabel.innerText = 'Blade content'; + contentInput.placeholder = 'Enter your Blade content'; + } + }); + } + @endif + })(); + \ No newline at end of file diff --git a/resources/dashboard.js b/resources/dashboard.js deleted file mode 100644 index b48f596..0000000 --- a/resources/dashboard.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Progressive enhancement when JavaScript is enabled to intercept form requests - * and instead handle them with an asynchronous Fetch instead of refreshing the page. - * - * @param {Element} form - * @param {?callback} okHandler - * @param {?callback} errorHandler - * @param {?callback} beforeCallHandler - * @param {?callback} afterCallHandler - */ -function registerAsyncForm(form, okHandler = null, errorHandler = null, beforeCallHandler = null, afterCallHandler = null) { - form.addEventListener("submit", function (event) { - // Disable default form submit - event.preventDefault(); - - if (beforeCallHandler) { - beforeCallHandler(); - } - - fetch("", { - method: "POST", - body: new FormData(event.target), - headers: new Headers({ - "Accept": "application/json", - }), - }).then(async response => { - if (response.ok) { - if (okHandler) { - okHandler(response); - } - } else { - if (errorHandler) { - errorHandler(response); - } else { - let asyncErrorToast = document.getElementById("asyncErrorToast"); - let asyncErrorToastHeader = document.getElementById("asyncErrorToastHeader"); - let asyncErrorToastBody = document.getElementById("asyncErrorToastBody"); - - asyncErrorToastHeader.innerText = `Error: ${response.status} ${response.statusText}` - asyncErrorToastBody.innerText = (await response.json()).error; - - bootstrap.Toast.getOrCreateInstance(asyncErrorToast).show() - } - } - }).catch(error => { - // Handle any network-related errors - console.error("Network error:", error); - }); - - if (afterCallHandler) { - afterCallHandler(); - } - }); -} - -function registerCreateFormModalHandlers() { - let createPageModal = null; - - document.addEventListener('DOMContentLoaded', () => { - createPageModal = new bootstrap.Modal('#createPageModal'); - }); - - const createPageForm = document.getElementById("createPageForm"); - const createPageFormSubmit = document.getElementById("createPageButton"); - const createPageFormError = document.getElementById("createPageFormError"); - const createPageFormErrorContents = document.getElementById("createPageFormErrorContents"); - - const okHandler = () => { - // let data = await response.json(); - createPageModal.hide(); - createPageForm.reset(); - - // Reload so new page shows up in the table - location.reload(); - }; - - const errorHandler = async response => { - let data = await response.json(); - createPageFormError.style.display = 'block'; - createPageFormErrorContents.innerText = data.error; - }; - - const beforeCallHandler = () => { - createPageFormSubmit.disabled = true; - createPageFormError.style.display = 'none'; - createPageFormErrorContents.innerText = ''; - }; - - const afterCallHandler = () => { - createPageFormSubmit.disabled = false; - }; - - registerAsyncForm(createPageForm, okHandler, errorHandler, beforeCallHandler, afterCallHandler); -} - -function handleModalInteractivity() { - // Focus when modal is opened - const modal = document.getElementById('createPageModal') - const firstInput = document.getElementById('pageTypeSelection') - - modal.addEventListener('shown.bs.modal', () => { - firstInput.focus() - }) - - // Handle form interactivity - - const createPageModalLabel = document.getElementById('createPageModalLabel'); - const titleInputLabel = document.getElementById('titleInputLabel'); - const contentInputLabel = document.getElementById('contentInputLabel'); - - const contentInput = document.getElementById('contentInput'); - const pageTypeSelection = document.getElementById('pageTypeSelection'); - const createPageButton = document.getElementById('createPageButton'); - - const baseInfo = document.getElementById('baseInfo'); - const createsPost = document.getElementById('createsPost'); - - const createPageModalLabelDefault = createPageModalLabel.innerText; - const titleInputLabelDefault = titleInputLabel.innerText; - const contentInputLabelDefault = contentInputLabel.innerText; - const contentInputPlaceholderDefault = contentInput.placeholder; - - pageTypeSelection.addEventListener('change', function (event) { - createPageModalLabel.innerText = createPageModalLabelDefault; - titleInputLabel.innerText = titleInputLabelDefault; - contentInputLabel.innerText = contentInputLabelDefault; - contentInput.placeholder = contentInputPlaceholderDefault; - - createPageButton.disabled = false; - createPageButton.title = ''; - - baseInfo.style.display = 'none'; - createsPost.style.display = 'none'; - - let selection = event.target.value; - - if (selection === 'markdown-post') { - baseInfo.style.display = 'block'; - createsPost.style.display = 'block'; - createPageModalLabel.innerText = 'Creating a new Markdown post'; - titleInputLabel.innerText = 'Post title'; - } else { - baseInfo.style.display = 'block'; - createPageModalLabel.innerText = 'Creating a new ' + selection.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase()); - } - - if (selection === 'blade-page') { - contentInputLabel.innerText = 'Blade content'; - contentInput.placeholder = 'Enter your Blade content'; - } - }); -} - -document.querySelectorAll(".buttonActionForm").forEach(form => { - registerAsyncForm(form); -}); - -registerCreateFormModalHandlers(); - -handleModalInteractivity(); diff --git a/src/Http/DashboardController.php b/src/Http/DashboardController.php index fdd3620..057df21 100644 --- a/src/Http/DashboardController.php +++ b/src/Http/DashboardController.php @@ -284,15 +284,6 @@ public function isInteractive(): bool return config('hyde.server.dashboard.interactive', true); } - public function getScripts(): string - { - if (PharSupport::running()) { - return file_get_contents('phar://hyde.phar/vendor/hyde/realtime-compiler/resources/dashboard.js'); - } - - return file_get_contents(Hyde::vendorPath('resources/dashboard.js', 'realtime-compiler')); - } - public function getFlash(string $key, $default = null): ?string { return $this->flashes[$key] ?? $default; From 8f90c95c84e1ab89f7242acf2f3a112d5b66116f Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 22:32:27 +0200 Subject: [PATCH 03/33] Fix modal opening --- resources/dashboard.blade.php | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index 68749fa..ccc8851 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -640,6 +640,9 @@ dialog#quickViewModal { width: min(960px, 94vw); height: min(720px, 88vh); + } + + dialog#quickViewModal[open] { display: flex; flex-direction: column; } @@ -1217,10 +1220,20 @@ function toTitleCase(value) { document.querySelectorAll('.quick-view-btn').forEach(button => { button.addEventListener('click', function () { - document.getElementById('quickViewModalLabel').innerText = this.dataset.previewLabel; - document.getElementById('quickViewOpenLink').href = this.dataset.previewUrl; - document.getElementById('quickViewFrame').src = this.dataset.previewUrl; - document.getElementById('quickViewModal').showModal(); + const modal = document.getElementById('quickViewModal'); + const label = document.getElementById('quickViewModalLabel'); + const openLink = document.getElementById('quickViewOpenLink'); + const frame = document.getElementById('quickViewFrame'); + + if (!modal || !label || !openLink || !frame) { + console.error('Quick view: one or more modal elements are missing from the DOM.'); + return; + } + + label.innerText = this.dataset.previewLabel || 'Preview'; + openLink.href = this.dataset.previewUrl || '#'; + frame.src = this.dataset.previewUrl || 'about:blank'; + modal.showModal(); }); }); From 7e68afd84ee6c20ad0530f54b7223ce2ad6ca52b Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 22:37:22 +0200 Subject: [PATCH 04/33] Prevent iframe recursion --- src/Http/DashboardController.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Http/DashboardController.php b/src/Http/DashboardController.php index 057df21..1e07292 100644 --- a/src/Http/DashboardController.php +++ b/src/Http/DashboardController.php @@ -259,6 +259,10 @@ public static function renderIndexPage(HydePage $page): string $contents = $page->compile(); } + if (self::isLoadedInIframe()) { + return $contents; + } + // If the page is the default welcome page we inject dashboard components if (str_contains($contents, 'This is the default homepage')) { if (config('hyde.server.dashboard.welcome-banner', true)) { @@ -279,6 +283,11 @@ public static function renderIndexPage(HydePage $page): string return $contents; } + protected static function isLoadedInIframe(): bool + { + return strtolower($_SERVER['HTTP_SEC_FETCH_DEST'] ?? '') === 'iframe'; + } + public function isInteractive(): bool { return config('hyde.server.dashboard.interactive', true); From 42ef093646ef9bd147911b808377ae17ecbb8411 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 22:37:47 +0200 Subject: [PATCH 05/33] Fix spacing issue --- resources/dashboard.blade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index ccc8851..b36f662 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -764,6 +764,8 @@ padding: 0; flex: 1; min-height: 0; + max-height: none; + overflow: hidden; } .quick-view-body iframe { From b296ef550b1b557f151468794083a870737cec03 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 22:50:44 +0200 Subject: [PATCH 06/33] Restyle the quick view button --- resources/dashboard.blade.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index b36f662..2ea85b9 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -205,6 +205,10 @@ font-size: 12px; } + .quick-view-btn { + margin-left: 0.25rem; + } + /* ---------- Layout / cards ---------- */ main { @@ -1065,8 +1069,8 @@ @endif @endif - Open + From b0c3a85ad0ed3963edd38041919fb57284f499d4 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 22:51:10 +0200 Subject: [PATCH 07/33] Update the title --- resources/dashboard.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index 2ea85b9..6fda918 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -865,7 +865,7 @@
-

Dashboard

+

Welcome to your site dashboard!

This page is served by the Realtime Compiler and won't be saved to your static site.

From ecc75b0c45071ea0598e0ef52ea62c4a3f07ed0d Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 23:05:05 +0200 Subject: [PATCH 08/33] Prettify the code --- resources/dashboard.blade.php | 198 ++++++++++++++++++++-------------- 1 file changed, 117 insertions(+), 81 deletions(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index 6fda918..0312e1c 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -9,34 +9,34 @@ + @@ -1007,11 +1013,17 @@ @endif +
@if(empty($dashboard->getPageList()))
No pages yet - There are no pages yet. @if($dashboard->isInteractive()) Create one using the button above. @else Why not create some? @endif + There are no pages yet. + @if($dashboard->isInteractive()) + Create one using the button above. + @else + Why not create some? + @endif
@else
@@ -1025,28 +1037,34 @@ + @php $typeColors = [ - 'BladePage' => 'red', - 'MarkdownPage' => 'blue', - 'MarkdownPost' => 'teal', + 'BladePage' => 'red', + 'MarkdownPage' => 'blue', + 'MarkdownPost' => 'teal', 'DocumentationPage' => 'amber', - 'HtmlPage' => 'orange', + 'HtmlPage' => 'orange', ]; @endphp + @foreach($dashboard->getPageList() as $route) @php $typeKey = class_basename($route->getPageClass()); $color = $typeColors[$typeKey] ?? 'purple'; @endphp - getFlash('justCreatedPage') === $route->getRouteKey() ? 'justCreatedPage just-created' : ''])> + getFlash('justCreatedPage') === $route->getRouteKey() ? 'justCreatedPage just-created' : ''])> + - - {{ $typeKey }} - + + {{ $typeKey }} + + {{ $route->getRouteKey() }} + @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) <none> @@ -1054,7 +1072,9 @@ {{ $route->getSourcePath() }} @endif + {{ $route->getOutputPath() }} +
@if($dashboard->isInteractive()) @@ -1062,6 +1082,7 @@ + @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) @else @@ -1069,10 +1090,12 @@ @endif @endif + Open
+ @endforeach @@ -1087,6 +1110,7 @@

Media library

+
@if(empty(\Hyde\Support\Filesystem\MediaFile::all()))
@@ -1109,6 +1133,7 @@ @endif
@endif +
Fullscreen @if($dashboard->isInteractive()) @@ -1121,6 +1146,7 @@ @endif
+
{{ $mediaFile->getExtension() }}
@@ -1128,6 +1154,7 @@
{{ $dashboard::bytesToHuman($mediaFile->getLength()) }}
+
@endforeach
@@ -1137,9 +1164,9 @@ @if($dashboard->showTips())
- - - + + +

Tip: {{ $dashboard->getTip() }}

@endif @@ -1199,6 +1226,7 @@ function toTitleCase(value) { await navigator.clipboard.writeText(this.dataset.copyValue); icon.style.display = 'none'; iconSuccess.style.display = 'inline'; + setTimeout(() => { icon.style.display = 'inline'; iconSuccess.style.display = 'none'; @@ -1256,6 +1284,7 @@ function showErrorToast(title, message) { document.getElementById('asyncErrorToastHeader').innerText = title; document.getElementById('asyncErrorToastBody').innerText = message; toast.classList.add('show'); + clearTimeout(toast._hideTimer); toast._hideTimer = setTimeout(() => toast.classList.remove('show'), 6000); } @@ -1283,7 +1312,7 @@ function registerAsyncForm(form, okHandler = null, errorHandler = null, beforeCa fetch('', { method: 'POST', body: new FormData(event.target), - headers: new Headers({'Accept': 'application/json'}), + headers: new Headers({ 'Accept': 'application/json' }), }).then(async response => { if (response.ok) { okHandler?.(response); @@ -1312,21 +1341,27 @@ function registerAsyncForm(form, okHandler = null, errorHandler = null, beforeCa const createPageFormError = document.getElementById('createPageFormError'); const createPageFormErrorContents = document.getElementById('createPageFormErrorContents'); - registerAsyncForm(createPageForm, () => { - document.getElementById('createPageModal').close(); - createPageForm.reset(); - location.reload(); - }, async response => { - const data = await response.json(); - createPageFormError.style.display = 'block'; - createPageFormErrorContents.innerText = data.error; - }, () => { - createPageButton.disabled = true; - createPageFormError.style.display = 'none'; - createPageFormErrorContents.innerText = ''; - }, () => { - createPageButton.disabled = false; - }); + registerAsyncForm( + createPageForm, + () => { + document.getElementById('createPageModal').close(); + createPageForm.reset(); + location.reload(); + }, + async response => { + const data = await response.json(); + createPageFormError.style.display = 'block'; + createPageFormErrorContents.innerText = data.error; + }, + () => { + createPageButton.disabled = true; + createPageFormError.style.display = 'none'; + createPageFormErrorContents.innerText = ''; + }, + () => { + createPageButton.disabled = false; + } + ); /* ---------- page type field switching ---------- */ @@ -1334,6 +1369,7 @@ function registerAsyncForm(form, okHandler = null, errorHandler = null, beforeCa const titleInputLabel = document.getElementById('titleInputLabel'); const contentInputLabel = document.getElementById('contentInputLabel'); const contentInput = document.getElementById('contentInput'); + const pageTypeSelection = document.getElementById('pageTypeSelection'); const baseInfo = document.getElementById('baseInfo'); const createsPost = document.getElementById('createsPost'); From aafd9d008286e01702d70b1f5bdd8a93aa30eefb Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 23:10:12 +0200 Subject: [PATCH 09/33] Remove padding override --- resources/dashboard.blade.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index 0312e1c..e5ab75b 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -205,10 +205,6 @@ font-size: 12px; } - .quick-view-btn { - margin-left: 0.25rem; - } - /* ---------- Layout / cards ---------- */ main { From 39585c2ddab165cd00f6b08148276033b8d3717d Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 23:22:37 +0200 Subject: [PATCH 10/33] Add a button to delete pages in the dashboard --- resources/dashboard.blade.php | 135 ++++++++++++++++++++++++++++++- src/Http/BaseController.php | 8 +- src/Http/DashboardController.php | 36 +++++++++ tests/RealtimeCompilerTest.php | 53 ++++++++++++ 4 files changed, 227 insertions(+), 5 deletions(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index e5ab75b..b90a313 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -165,6 +165,11 @@ text-decoration: none; } + .btn:disabled { + opacity: .45; + cursor: not-allowed; + } + .btn svg { width: 14px; height: 14px; @@ -188,6 +193,18 @@ cursor: not-allowed; } + .btn-danger { + background: var(--red-soft); + border-color: rgba(240, 87, 92, .34); + color: #ffb3b5; + } + + .btn-danger:hover { + background: rgba(240, 87, 92, .22); + border-color: var(--red); + color: #ffd0d1; + } + .btn-ghost { background: transparent; border-color: transparent; @@ -633,6 +650,10 @@ max-height: 88vh; } + dialog#deletePageModal { + width: min(460px, 92vw); + } + dialog#quickViewModal { width: min(960px, 94vw); height: min(720px, 88vh); @@ -754,6 +775,23 @@ margin-bottom: 16px; } + .delete-page-details { + margin: 0; + color: var(--text-muted); + font-size: 13px; + } + + .delete-page-details strong { + color: var(--text); + } + + .delete-page-details .mono { + display: inline-block; + margin-top: 8px; + color: #ffb3b5; + word-break: break-all; + } + /* Quick view */ .quick-view-body { @@ -1085,6 +1123,18 @@ @endif + + @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) + + @else + + @endif @endif Open @@ -1191,6 +1241,38 @@ @if($dashboard->isInteractive()) + +
+ + + + + + + + + +
+
+
From dce8b7bad7a3a546be7eb02737dd7e272d16cef0 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Thu, 9 Jul 2026 23:57:26 +0200 Subject: [PATCH 14/33] Create an improved route list --- resources/dashboard.blade.php | 279 ++++++++++++++++++++++------------ 1 file changed, 179 insertions(+), 100 deletions(-) diff --git a/resources/dashboard.blade.php b/resources/dashboard.blade.php index b137a32..ae2edc2 100644 --- a/resources/dashboard.blade.php +++ b/resources/dashboard.blade.php @@ -348,70 +348,119 @@ height: 14px; } - /* ---------- Table ---------- */ + /* ---------- Route list ---------- */ - .table-scroll { + .route-list { overflow-x: auto; } - table { - width: 100%; - border-collapse: collapse; + .route-group + .route-group { + margin-top: 6px; } - thead th { - text-align: left; + .route-group-label { + display: flex; + align-items: center; + gap: 8px; font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); - padding: 0 12px 10px; - white-space: nowrap; + padding: 16px 12px 8px; + } + + .route-group-label .count { + font-weight: 500; + letter-spacing: 0; + text-transform: none; + color: var(--text-faint); } - tbody td { - padding: 11px 12px; + .route-row { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 12px; border-top: 1px solid var(--border-soft); - font-size: 13px; - vertical-align: middle; } - tbody tr:hover { + .route-row:hover { background: var(--surface-2); } - .path-cell { + .route-file { + flex: 1; + min-width: 0; + } + + .route-path-line { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + white-space: nowrap; + } + + .route-path-line svg { + width: 14px; + height: 14px; + flex-shrink: 0; + color: var(--text-faint); + } + + .route-path-src { + color: var(--text); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + } + + .route-path-out { color: var(--text-muted); - max-width: 260px; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; } - .type-pill { - display: inline-flex; + .route-meta-line { + display: flex; align-items: center; gap: 6px; - font-size: 12px; - font-family: var(--font-mono); - padding: 3px 9px; - border-radius: 6px; + margin-top: 4px; + font-size: 11px; + color: var(--text-faint); } - .type-pill .dot { + .route-meta-line .dot { width: 6px; height: 6px; border-radius: 50%; + flex-shrink: 0; + } + + .route-key-col { + width: 160px; + flex-shrink: 0; + font-size: 13px; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - .row-actions { + .route-actions-col { display: flex; justify-content: flex-end; - gap: 6px; + gap: 4px; + flex-shrink: 0; } - .just-created td { + .route-actions-col .btn svg { + width: 15px; + height: 15px; + } + + .just-created { animation: rowFadeIn 2.2s ease-out; } @@ -476,8 +525,8 @@ justify-content: center; gap: 11px; background: - radial-gradient(circle at 35% 20%, rgba(var(--media-brand-rgb), .32), transparent 42%), - linear-gradient(135deg, rgba(var(--media-brand-rgb), .20), rgba(var(--media-brand-rgb), .08)); + radial-gradient(circle at 35% 20%, rgba(var(--media-brand-rgb), .32), transparent 42%), + linear-gradient(135deg, rgba(var(--media-brand-rgb), .20), rgba(var(--media-brand-rgb), .08)); } .media-preview .file-preview-mark { @@ -884,7 +933,7 @@ padding-right: 16px; } - .row-actions { + .route-actions-col { flex-wrap: wrap; } @@ -1081,92 +1130,122 @@ @endif @else -
- - - - - - - - - - - - - @php - $typeColors = [ - 'BladePage' => 'red', - 'MarkdownPage' => 'blue', - 'MarkdownPost' => 'teal', - 'DocumentationPage' => 'amber', - 'HtmlPage' => 'orange', - ]; - @endphp - - @foreach($dashboard->getPageList() as $route) - @php - $typeKey = class_basename($route->getPageClass()); - $color = $typeColors[$typeKey] ?? 'purple'; - @endphp - getFlash('justCreatedPage') === $route->getRouteKey() ? 'justCreatedPage just-created' : ''])> - - - - - - +
+ @php + $typeColors = [ + 'BladePage' => 'red', + 'MarkdownPage' => 'blue', + 'MarkdownPost' => 'teal', + 'DocumentationPage' => 'amber', + 'HtmlPage' => 'orange', + ]; + + $groups = []; + foreach ($dashboard->getPageList() as $route) { + $isMemory = $route->getPage() instanceof \Hyde\Pages\InMemoryPage; + + if ($isMemory) { + $dir = 'Generated'; + } else { + $sourcePath = $route->getSourcePath(); + $dir = str_contains($sourcePath, '/') ? explode('/', $sourcePath, 2)[0] : $sourcePath; + } + + $groups[$dir][] = $route; + } + + // Keep source directories in a stable order, with dynamically generated routes listed last. + uksort($groups, function ($a, $b) { + if ($a === 'Generated') return 1; + if ($b === 'Generated') return -1; + return strcmp($a, $b); + }); + @endphp + + @foreach($groups as $dir => $routes) +
+
{{ $dir }} {{ count($routes) }}
+ + @foreach($routes as $route) + @php + $typeKey = class_basename($route->getPageClass()); + $color = $typeColors[$typeKey] ?? 'purple'; + $isMemory = $route->getPage() instanceof \Hyde\Pages\InMemoryPage; + @endphp + +
getFlash('justCreatedPage') === $route->getRouteKey() ? 'justCreatedPage just-created' : ''])> + +
+
+ @if($isMemory) + + generated + @else + + {{ $route->getSourcePath() }} + @endif -
+ - + + + - - @endforeach - -
Page typeRoute keySource fileOutput file
- - {{ $typeKey }} - - {{ $route->getRouteKey() }} - @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) - <none> - @else - {{ $route->getSourcePath() }} - @endif - {{ $route->getOutputPath() }} -
+ {{ $route->getOutputPath() }} +
+ +
+ + {{ $typeKey }} +
+ + +
{{ $route->getRouteKey() }}
+ +
@if($dashboard->isInteractive())
- @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) - + @if($isMemory) + @else - + @endif
- @if($route->getPage() instanceof \Hyde\Pages\InMemoryPage) - + @if($isMemory) + @else @endif @endif - Open - -
-
+ +
+ + @endforeach + + @endforeach @endif @@ -1194,10 +1273,10 @@ class="btn btn-danger btn-sm delete-page-btn" @else @php($mediaPlaceholder = $dashboard::getMediaPlaceholder($mediaFile->getExtension()))