Skip to main content

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 selected in the server render. Below it WireKit's select ignores value, 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-dialog cancel 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-0 and 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-icons installed without blade-heroicons is enough, and the shipped screens ask for heroicon-* 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.json carries "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 by WirekitFloorContractTest — 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
They are a pair — half of it costs you the iconography

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:

ScriptEmitted whenCan it be dropped?
The theme = 'auto' dark-mode mirrorui.theme is autoYes — 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=light or =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.)