{{-- data-bs-theme is stamped alongside data-theme and always carries the same value. data-theme drives this app's own --ax-* tokens (css/style.css); data-bs-theme is what flips Bootstrap 5.3's own theme variables — --bs-emphasis-color (which .table reads for its cell colour), --bs-secondary-bg / --bs-tertiary-bg, the .bg-*-subtle / *-text-emphasis families — plus the handful of rules it keys off the attribute directly (.btn-close's invert filter, .form-select's caret SVG, .form-switch's knob, .accordion-button's chevron). Without it those all stayed on their light-mode values under the dark theme, which is how .table body text ended up black on #10131a. style.css still wins wherever it defines a --bs-* variable itself: it loads after bootstrap.min.css, and :root[data-theme="dark"] outranks Bootstrap's [data-bs-theme=dark] on specificity, so the app palette keeps overriding Bootstrap's neutral-grey dark defaults rather than the other way round. --}} @yield('title', \App\Support\Branding::name()) {{-- Preconnect so the font files aren't queued behind the stylesheet request. --}} {{-- Loaded once here rather than per-page: enough pages use bi-* icons now (dashboards, stat tiles, empty states, the sidebar) that pushing this from each of them was just duplicating the same tag. --}} @stack('styles') {{-- Mirrors the DB-backed theme into this browser's localStorage, so the pages that have no user to read a preference from — the pre-auth login page, and the error pages, which must not touch the DB at all — can fall back to it instead of always defaulting to the OS preference and the stock blue accent. --}} {{-- Ported from the legacy layout.php + layout_footer.php + header.php + sidebar.php + footer.php (repo root) — see the Phase 4 plan. $role is computed once here and shared with the header/sidebar partials instead of each one re-reading $_SESSION directly. --}} @php($role = \App\Support\SessionAuth::role()) {{-- Navigation progress bar. Sits before the header so it's the first thing painted. --}} @include('layouts.partials.header', ['role' => $role]) @include('layouts.partials.sidebar', ['role' => $role])
{{-- Replaces the legacy App\Helpers\Flash::render() — these pages use Laravel's own session for flash messages (see the Phase 2 plan), not the legacy session. Rendered as toasts rather than inline alerts: every flashing controller in the app funnels through this one block, so they all get the same treatment without any controller changes. Full-width alerts here used to push the whole page down on arrival, which was especially disruptive mid-task. Only page-level flashes live here. The per-form AJAX feedback driven by data-alert-target (see the js-ajax-form handler below) stays inline on purpose — it belongs next to the control that changed, often inside an open modal, and moving it to a screen corner would be a downgrade. --}} {{-- NB: the flash list is written inline in the loop below rather than hoisted into a raw-PHP block. This layout already uses the inline single-line form of that directive further up (for $role), and mixing it with the block form here made Blade emit an unterminated opening tag for it, turning the rest of the layout into literal PHP and 500-ing every page that extends it. Keep raw-PHP blocks out of this file. --}} @if (session()->hasAny(['success', 'info', 'warning', 'error']))
@foreach ([ 'success' => ['icon' => 'bi-check-circle-fill', 'auto' => true], 'info' => ['icon' => 'bi-info-circle-fill', 'auto' => true], 'warning' => ['icon' => 'bi-exclamation-triangle-fill', 'auto' => false], 'error' => ['icon' => 'bi-x-circle-fill', 'auto' => false], ] as $flashType => $meta) @if (session($flashType)) {{-- Successes and info clear themselves; warnings and errors wait for a deliberate dismiss, since a failure the user scrolled past unnoticed is worse than a toast that lingers. data-bs-delay is always emitted; Bootstrap ignores it when autohide is false. --}}
{{ session($flashType) }}
@endif @endforeach
{{-- The .show class above makes a toast visible without JS (so the message is never lost if Bootstrap fails to load), but the autohide timer only runs once the toast is instantiated — hence this. --}} @endif {{-- Navigation progress bar. This app is server-rendered, so between clicking a link and the next document painting there is no feedback at all — on the slower report pages that reads as an unresponsive UI. The bar is indeterminate by necessity: a normal navigation emits no progress events, so it eases toward 90% and is simply discarded when the new document replaces it. Most of the logic here is about NOT starting the bar, since a bar that starts without a navigation following would sit on screen forever: - .js-ajax-form submits are intercepted by the handler below and never navigate (those get a button spinner instead); - a submit already cancelled by an onsubmit="return confirm(...)" arrives here with defaultPrevented set; - clicks that open a new tab, trigger a modal, start a download, or are modifier/middle-clicked don't navigate this document either. --}} {{-- Shared handler for "confirm, delete/approve one row, don't reload the page" forms (Phase: AJAX-ify confirm-then-redirect forms). Opt in per-form via class="js-ajax-form"; the controller action must branch on the X-Requested-With header and return JSON ({success, message, ...extra}) instead of its normal redirect when present — every non-AJAX caller keeps working exactly as before. data-remove-target: CSS selector for the element to remove from the DOM on success (defaults to "tr"). Resolved via form.closest(...) — the usual case of a form nested inside the row it deletes — unless the selector starts with "#", in which case it's resolved via document.querySelector(...) instead, for the rarer case where the form lives outside the row it controls (e.g. a hidden form linked to its button only via the button's form="..." attribute). data-replace-target: alternative to data-remove-target for forms whose action changes rather than removes content (add/edit/reorder) — the server returns a re-rendered HTML fragment as data.html, and that fragment replaces the target element's outerHTML instead of removing it. Same "#"-prefix vs. closest(...) resolution rule as data-remove-target. If the submitting form sits inside a currently-open Bootstrap modal (edit/add dialogs), that modal is hidden first and the DOM swap waits for its 'hidden.bs.modal' event — swapping immediately would tear out the modal element mid-transition and leave its backdrop stuck. data-alert-target: optional selector for a container to show inline success/error feedback in. data-keep-modal-open: set on a form to skip the auto-close-enclosing-modal step above. Needed for a delete/remove-row form that lives inside a modal listing several rows (e.g. student profile's "All payments" modal) — that modal is a browsing surface for many rows, not the form's own dialog, so acting on one row shouldn't close it out from under the user. Leave unset for the common case where the form IS the modal's reason to exist (an add/edit dialog), which should close on success. Respects e.defaultPrevented so a sibling onsubmit="return confirm(...)" handler that already cancelled the event (user clicked Cancel) stops this from firing too — per spec, an onsubmit returning false calls preventDefault() but does not stop other submit listeners from running, so this check is required, not defensive-for-no-reason. Pages needing extra DOM sync beyond "remove this row" (running totals, pending-count badges, etc.) listen for the bubbling 'ajax-form:success' event on the form and patch their own page-specific bits from event.detail. A controller may also answer {success:false, needsConfirm:true} instead of {success:true/false, message} — see the needsConfirm branch below for why and what happens (real form.submit() fallback, no JSON round-trip on the resubmit). --}} @yield('content')
{{-- /ax-content /content-wrapper --}} @include('layouts.partials.footer')
{{-- /ax-page-wrapper --}} @stack('scripts')