rivalex/clearance
A drop-in Laravel admin panel for spatie/laravel-permission. Manage permissions, roles, guards and settings via Livewire 4 + Flux UI, with contextual authorization, parent-bounded ceiling roles, and optional super admin.
ClearanceServiceProvider::warnIfPermissionModelMissingTrait() compared the app's configured permission.models.permission class against Clearance's own Rivalex\Clearance\Models\Permission only. Apps that never opt into Clearance's model — the default, common case, using Spatie's stock Spatie\Permission\Models\Permission unmodified — were misreported as running a "custom" model missing HasPermissionGroups, logging Log::warning on every request. Added an explicit exemption for Spatie's default model alongside Clearance's own. New tests/Unit/T68PermissionModelWarnTest.php covers: default Spatie model (no warning), Clearance's own model (no warning), genuine custom model without the trait (warns). See SPEC.md §B3.rivalex/lingua. Clearance's own CSS uses a class-based dark: variant that follows the host app's <html class="dark"> ancestor; hosts with no class-based dark toggle (or whose real OS/browser scheme doesn't match a deliberately-dark host layout) previously rendered Clearance unstyled/illegible even though the rest of the page was fine. config('clearance.dark_mode.force') (env CLEARANCE_FORCE_DARK_MODE) sets the default; a new "Appearance" card in /clearance/settings (Livewire\Settings\AppearanceSettings, DB-backed via ClearanceSettings::get/set('dark_mode.force', ...)) overrides it live, same DB-over-config precedence as Lingua. New anonymous component <x-clearance::dark-scope> (resources/views/components/dark-scope.blade.php) wraps the resolved value in an optional .dark ancestor div around the existing .clearance scope div; every one of the 25 Blade views that previously opened with a bare <div class="clearance ..."> now opens with <x-clearance::dark-scope ...> instead (content and any extra utility classes on the wrapper preserved 1:1).dark: variant was OS-preference-based, not class-based — resources/css/clearance.css imported tailwindcss without ever declaring [@custom-variant](https://github.com/custom-variant) dark (&:where(.dark, .dark *)); (present in Lingua's CSS entry, missing here), so every dark: utility Clearance itself authors compiled under Tailwind v4's default strategy — [@media](https://github.com/media) (prefers-color-scheme: dark) — completely ignoring any .dark class on <html>. Confirmed live against rivalex.test/clearance: with <html class="dark"> already present (this host's layout is deliberately dark-only), borders/nav pills/muted text using Clearance's own dark: classes still rendered in their light-mode colors, because the compiled CSS gated them on the OS color-scheme media query instead. Added the same [@custom-variant](https://github.com/custom-variant) dark declaration Lingua uses; rebuilt src/dist/css/clearance.min.css now contains zero prefers-color-scheme rules and the expected :where(.dark, .dark *) selectors, verified against the live page (border/nav theming now correct with force/.dark set, independent of OS scheme).<flux:card> stat tiles rendered an unstyled white background with invisible (white-on-white) numbers, even after the [@custom-variant](https://github.com/custom-variant) dark fix above. Root cause is narrower and unrelated to the OS-vs-class issue: Flux's own component markup (vendor/livewire/flux/stubs/resources/views/flux/card/index.blade.php) uses a compound utility, dark:[:where(&)]:bg-white/10 (a custom dark: variant stacked on an arbitrary [:where(&)]: bracket variant). Confirmed both livewire/flux (v2.15.0) and tailwindcss/[@tailwindcss](https://github.com/tailwindcss)/vite (v4.3.3) are already latest, so this isn't a version-lag issue — verified directly against the compiled CSS that sibling utilities from the same Flux template line compile fine individually ([:where(&)]:bg-white, [:where(&)]:p-6, [:where(&)]:rounded-xl, plain-stacked dark:border-white/10), but the doubly-stacked dark:[:where(&)]:bg-white/10 never gets generated at all — a genuine Tailwind v4 limitation combining a [@custom-variant](https://github.com/custom-variant)-defined variant with an arbitrary bracket variant on the same utility. Worked around in resources/css/clearance.css with a plain (non-arbitrary) [data-flux-card] { [@apply](https://github.com/apply) dark:bg-white/10; } override, keyed off the stable data-flux-card attribute Flux's card root div already carries — compiles correctly (:where(.dark, .dark *) .clearance [data-flux-card]) and doesn't touch light mode. Verified live: all four dashboard stat tiles now show correct numbers and translucent-dark backgrounds.update-changelog.yml CI workflow hardcoded ref: main (both the actions/checkout step and stefanzweifel/git-auto-commit-action's branch:) — this repo's only branch is master (git ls-remote --heads origin confirms no main ref exists), so every run since this workflow was added failed at checkout (git fetch ... refs/heads/main* finding nothing, retried 3x, exit 1). Both hardcoded to master.codecov/patch failing at 60.00% vs a 78.02% target on the dark-mode commit — new AppearanceSettings.php shipped with zero tests. Added tests/Unit/T67AppearanceSettingsTest.php (mount defaults, config-fallback, DB-overrides-config precedence, save persistence for both true/false) — Livewire/Settings/AppearanceSettings now at 100% coverage; full suite 381/381 passing.phpstan CI job was failing with ~90 errors across src/, none related to the permission-naming fix above; root cause was almost entirely that Spatie's Role/Permission relations resolve their related model via config() at runtime, so static analysis only sees the base Illuminate\Database\Eloquent\Model type instead of Permission/Role, cascading into "undefined property/method" everywhere those collections were touched, plus a package-boundary gap where nothing in src/ can statically know the consuming app's User model composes Spatie's HasRoles trait. Fixed for real, no baseline/[@phpstan-ignore](https://github.com/phpstan-ignore) dumping:
RoleMeta, UserRoleContext, UserContextPermissionOverride gained [@property](https://github.com/property) docblocks and [@return](https://github.com/return) BelongsTo<X, $this>/HasMany<X, $this> generics on their relations, matching the actual migrated schema.Collection::whereInstanceOf() narrows Spatie relation collections ($role->permissions, etc.) from generic Model to the real Permission/Role class before they're iterated or type-hinted in closures — zero-op at runtime, every row already is one.PermissionService::create() / RoleService::create() narrow Spatie's own imprecise [@return](https://github.com/return) Contract|Model (not static) via a real instanceof check + throw, instead of trusting an unprovable cast.Rivalex\Clearance\Contracts\ClearanceAuthenticatable marker interface documents the subset of HasRoles/HasPermissions this package calls on the consumer's User model; resolveUser() in the five Livewire\Users\* components verifies it at runtime (instanceof Authenticatable + method_exists(..., 'roles')) before narrowing, replacing five copies of an invalid [@var](https://github.com/var) \Spatie\Permission\Traits\HasRoles $user (PHPStan correctly rejected casting to a trait).RoleForm.php: permissionGroups' PHPDoc array shape was missing the locked/out_of_ceiling keys that loadPermissions() has always actually set — runtime was never broken (the ?? false silently no-op'd), but the stale annotation hid a real shape mismatch.use App\Models\User; imports (Settings.php, BulkAssignDefaultRole.php) — package has no such class; the fallback default is now the literal FQCN string, which was always the intent.HasClearance (a mixin trait for the consuming app's User model, never used inside this package) is now in phpstan.neon.dist's excludePaths instead of silently failing trait.unused; the stale ignoreErrors entry for Builder (dead since none of the above patterns matched it anymore) was removed.view('clearance::...') call (~38 occurrences across every Livewire component) failed CI's PHPStan run — but never reproduced locally, even from a fresh composer install — as expects view-string|null, string given. Larastan's view-string type validates the literal against a real, booted View::exists() call; this package has no bootstrap/app.php for Larastan to boot on its own, and the clearance:: view namespace is registered by spatie/laravel-package-tools' hasViews() at runtime inside ClearanceServiceProvider::configurePackage() — invisible to any static parse. Added tests/phpstan-bootstrap.php (wired via phpstan.neon.dist's bootstrapFiles) that boots the package's own tests/TestCase.php before analysis, which registers the same providers the real test suite uses and resolves the namespace for real. Orchestra\Testbench\Foundation\Application::create() was tried first with both a resolvingCallback (fires before the container's config binding exists — "Target class [config] does not exist") and an options: ['providers' => [...]] array (boots clean but never calls configurePackage(), so the namespace still never registers) before landing on reusing TestCase. One more gap surfaced once the app booted: Larastan's console-command analysis of ClearanceInstallCommand needs command.tinker, a DeferrableProvider binding that only auto-loads via the full package manifest from a real composer install — registered eagerly in the bootstrap file instead._ in segments (V6) — PermissionService::validate()'s gruppo-azione regex constrained each segment to [a-z0-9], so any name containing _ was rejected as an invalid symbol regardless of naming_separator config (e.g. user_profile-manage failed). Segments now accept [a-z0-9_]; the separator itself, spaces, dots, and all other symbols remain rejected. Compounding client-side bug: the custom-ability Alpine slugify() helper in permission-form.blade.php stripped _ to - before the value ever reached the server, so typing an underscore into the custom-ability pill input silently became a dash even after the backend fix. Both the regex (src/Services/PermissionService.php) and the JS slugifier (resources/views/livewire/permissions/permission-form.blade.php) now preserve _. SPEC.md §C and V6 updated to document underscore as a valid segment character. New Pest cases in T3PermissionServiceTest cover accepted underscore names and rejected other-symbol names (@, #, space, !).[1.0.0] - 2026-07-03 First public release. rivalex/clearance is a Livewire 4 + Flux UI admin panel for managing permissions, roles, and contextual authorization on top of spatie/laravel-permission, installable via a single artisan command.
Requirements PHP ^8.3 · Laravel ^12.0|^13.0 · spatie/laravel-permission ^6.0 · livewire/livewire ^3.0|^4.0 · livewire/flux ^2.14.
How can I help you explore Laravel packages today?