Styling the UI
Required for every screen this package renders — the dashboard, the self-service portal and both publishable stubs.
WireKit ≥ 2.38, and this one is enforced. The shipped screens depend on behavior WireKit gained in named releases, and every one of them fails quietly below its 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.38 is therefore the floor, and
composer.jsoncarries"conflict": {"pushery/wirekit": "<2.38"}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 floor above closes it, and 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 one inline script, and it 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 |
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 package emits no inline script at all. Nothing further to configure. - Keep
auto— the reader's system preference keeps being mirrored — and give that one script a nonce.
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.)