Styling the UI
Required for every screen this package renders — the dashboard, the self-service portal and both publishable stubs.
WireKit ≥ 2.53, and this one is enforced. The shipped screens depend on behavior WireKit gained in named releases. All but the last of them fail quietly below their version — the page renders, it just tells the reader something untrue.
2.12 — the dashboard and self-service tables sort their columns through WireKit's native keyboard-operable
sort-action(WCAG 2.1.1). Below it the sort headers still render, but only as mouse targets.2.25 — the self-service payload-version select marks the stored version
selectedin the server render. Below it WireKit's select ignoresvalue, so the field shows the first option — the empty "inherit" entry — for an endpoint that is pinned to a version, on the first paint and for every reader without JavaScript.2.27 — WireKit ships all seven locales this package ships, and wraps its own
alert-dialogcancel label in__(). Below it, five of those seven fall back to English on a button that still renders — in the wrong language, with nothing turning red.2.37 — the geometry that makes an overlay an overlay (
fixed inset-0and its z-index) ships in WireKit's own stylesheet instead of existing only as Tailwind utilities your build has to compile. Below it, the shipped destructive confirmations — delete endpoint in the self-service portal, rotate secret in the management stub — render correctly styled, visible and enabled, while sitting in normal document flow: on a page taller than the viewport the confirm button is off-screen and the click never lands. No error, no log line, nothing to tell an operator why. Whether it happened depended on your Tailwind build, which is what the two registrations below are for — so a misconfiguration one paragraph away turned a delete button into a dead one.2.38 — an icon whose alias resolves onto a set nobody registered degrades to the inert placeholder instead of throwing. Below it that case took the page down:
blade-iconsinstalled withoutblade-heroiconsis enough, and the shipped screens ask forheroicon-*names, so the dashboard and the portal answered 500 on every screen that draws an icon — which, through buttons and dropdowns, is every screen. The unknown-alias path already degraded; it was the resolved-alias-missing-set path that did not.2.44 — a code block becomes a landmark only when the caller gives it a name. Below it every code block was one, named after its language, so the self-service transform editor — which shows the sample payload and the transformed result side by side, both JSON — rendered two regions both called "json code". Two landmarks of one name is
landmark-unique, a WCAG-mapped failure that an automated audit of your own application reports against your pages, not ours. The screen looks right either way: a duplicate region name is something a screen-reader rotor shows and a sighted reader never sees.2.51 — WireKit's
paginationcan turn a page inside a Livewire component, and it ships the two views (wirekit::pagination.livewire,…livewire-simple) that Livewire'spaginationView()andpaginationSimpleView()point at. The shipped screens use them, so this is the one entry on this list that does not fail quietly: below it the view name does not resolve and every paginating screen throws rather than misinforms. It earns its place anyway, because of what it replaced — before it, WireKit's pager could only page by navigating, which on a filtered table meant page two came back unfiltered and any component state not held in the URL was gone.2.53 — that same pager stopped dimming its inert boundary control with an
opacityon top of an already-muted pair. Opacity applies to the ELEMENT, so text and background composite against the page together and the distance between them collapses: a 4.5:1 pair read at about 1.6:1 in the light theme. This package had removed exactly that line from its own control after measuring it, so adopting WireKit's pager at 2.51 would have quietly handed the property back.2.53 is therefore the floor, and
composer.jsoncarries"conflict": {"pushery/wirekit": "<2.53"}so it is a fact rather than a claim. That refuses an older WireKit; it never installs one, so a headless host stays headless. Every occurrence of the number is held together byWirekitFloorContractTest— the constraint and the prose cannot drift apart.
The package ships no compiled stylesheet. Its views are Tailwind utilities over WireKit's
design tokens: @wirekitStyles brings the tokens, and your Tailwind build compiles the
utilities that consume them. That build has to be told where to look — for WireKit's
components and for this package's views. Both source registrations are required; with either
one missing the screens render unstyled.
That is the visible half. The half that cost a release: until WireKit 2.37 the geometry of every overlay came from those same compiled utilities, so a missing registration also left the confirmation dialogs in normal document flow — styled, visible, and unreachable below the fold. The floor above closes that case whatever your build scans; the registrations are still required for everything else on the screen.
/* resources/css/app.css */
@import 'tailwindcss';
/* This package's views. */
@import '../../vendor/pushery/webhooks-for-laravel/resources/css/webhooks.css';
/* WireKit's components (required by every WireKit consumer — see its install notes). */
@source '../../vendor/pushery/wirekit/resources/views/**/*.blade.php';
The icon set
Install the icon set the screens draw their empty states and primary actions against. Without it WireKit renders an inert placeholder where each icon would be — the pages still work, they simply lose their iconography:
composer require blade-ui-kit/blade-icons blade-ui-kit/blade-heroicons
blade-icons is the renderer and blade-heroicons is the set the shipped screens ask for.
With either one missing the screens draw WireKit's inert placeholder where an icon belongs:
they render, they are simply without iconography, and both halves cost the same.
Installing the renderer alone is the easy accident — a host that already uses blade-icons for
its own icons reads the second package as optional. Until WireKit 2.38 that combination took the
page down rather than degrading. The enforced floor sits above that release, so what remains
is missing icons.
php artisan webhooks:preflight names the missing half.
Dark mode
WireKit's dark tokens live behind a .dark class on the document root. Because the dashboard
and the portal are the package's layouts, the package puts it there: ui.theme is auto by
default, which mirrors the reader's system preference (and keeps mirroring it if they change
it).
Pin it with WEBHOOKS_UI_THEME=light or =dark — which is also how you switch off the small
inline head script under a strict Content-Security-Policy.
Two package-level custom properties retune the plot heights on the dashboard without forking a
view: --wh-chart-height and --wh-sparkline-height.
On another UI kit entirely
Publish the views (--tag=webhooks-views, --tag=webhooks-dashboard-views,
--tag=webhooks-self-service-views) and restyle them; the pagination control
(webhooks::pagination) publishes with them. See the
publishable tags reference.
Embedding in an app with its own asset pipeline and a strict CSP
The shipped layouts emit only WireKit's tokens (@wirekitStyles), so an app with its own Vite
build has nowhere to load its compiled CSS — and its @source glob (above) has to reach
vendor/ for the utilities to build at all. Rather than publishing and forking the layout,
point ui.assets at a Blade partial and the layouts @include it in <head>:
// config/webhooks.php
'ui' => [
'assets' => 'webhooks-assets', // resources/views/webhooks-assets.blade.php: @vite(['resources/css/app.css'])
],
Under a strict Content-Security-Policy the package emits exactly two inline scripts in its own layouts, and one of them can be switched off:
| Script | Emitted when | Can it be dropped? |
|---|---|---|
The theme = 'auto' dark-mode mirror | ui.theme is auto | Yes — pin the theme with WEBHOOKS_UI_THEME=light/dark |
The wire:submit guard | always | No — it is a constant, so allow it by its hash |
The guard holds a Livewire form still until Livewire has bound it. A <form wire:submit> has no
method, so before the scripts at the end of the body arrive, Enter sends the page to its own
address as a GET, and every named field lands in the query string, the browser history and your
access log. Its hash is the same on every response, so it goes into your policy once:
use Pushery\Webhooks\Support\LivewireSubmitGuard;
// e.g. with spatie/laravel-csp, or wherever your policy is built:
$policy->add(Directive::SCRIPT, LivewireSubmitGuard::cspHash()); // 'sha256-…'
The layouts also put the theme nonce on it when you configure one, but prefer the hash: a nonce stops
matching the document's policy after wire:navigate and on a cached page.
Everything else the package runs in the browser is a file served from your own origin. The
three Alpine components — the delivery drawer's focus trap, the secret panel's countdown and the
endpoint form's focus handover — live in one script the package serves through its own route, and the views load it with
@assets <script src="…" defer>. Under script-src 'self' it needs no nonce and no
vendor:publish.
They are files rather than inline blocks on purpose: registering the behavior is what keeps it
working under a policy without unsafe-eval, where Alpine parses directive attributes against
a restricted grammar instead of evaluating them — an inline x-data object simply never runs
there, with nothing in any server log to say so. It would take out the drawer's focus trap and
leave the revealed secret on screen past its window.
So there are two ways through, and either one is enough on its own:
- Pin the theme (
WEBHOOKS_UI_THEME=lightor=dark) and the only inline script left is the submit guard, which its hash admits. - Keep
auto— the reader's system preference keeps being mirrored — and give the theme script a nonce, next to the guard's hash.
style-src — the one thing that is not solved
The scripts are handled. Styles are not, and the package says so rather than letting you find
out from an empty chart. The two dashboard plots — the hourly activity chart and the p95
sparkline — set each bar's height with a style attribute, because the height is a number that
comes from your data and a utility class cannot carry one.
A nonce does not help here, and that is not a package limitation: a nonce applies to <style>
elements, never to style attributes. Under style-src 'self' without 'unsafe-inline' a
browser drops them, and the bars collapse — an operator reads an empty plot as "nothing
happened in this window", with nothing in any server log to say otherwise.
So on a policy that strict, either allow 'unsafe-inline' for style-src (or hash the
attributes with style-src-attr 'unsafe-hashes'), or leave the dashboard out of the routes that
policy covers. Every constant color has been moved into a class, so what is left in an
attribute is only the geometry.
Note that WireKit itself emits one inline <style> block for its font custom properties, which
the same policy discards — so a strict style-src affects more than these two panels.
Because a per-request nonce is a closure and a closure in config breaks
php artisan config:cache, register it from a service provider instead of the config file:
// A service provider's boot():
use Pushery\Webhooks\Support\UiTheme;
UiTheme::resolveNonceUsing(fn () => \Illuminate\Support\Facades\Vite::cspNonce());
(config('webhooks.ui.csp_nonce') still accepts a static string for the rare case a fixed
nonce is enough.)