wsmallnews / support
Wsmallnews system support modules
Fund package maintenance!
Requires
- php: ^8.2
- filament/filament: ^4.0 || ^5.0
- filament/spatie-laravel-media-library-plugin: ^4.0 || ^5.0
- filament/spatie-laravel-settings-plugin: ^4.0 || ^5.0
- filament/spatie-laravel-tags-plugin: ^4.0 || ^5.0
- intervention/image-laravel: ^1.0
- overtrue/easy-sms: ^3.0
- phiki/phiki: ^2.2
- ralphjsmit/livewire-urls: ^1.0
- spatie/laravel-activitylog: ^5.0
- spatie/laravel-markdown: ^2.8
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- larastan/larastan: ^2.0 || ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^7.0 || ^8.0
- orchestra/testbench: ^9.0 || ^10.0
- pestphp/pest: ^2.0 || ^3.0 || ^4.0
- pestphp/pest-plugin-arch: ^2.0 || ^3.0 || ^4.0
- pestphp/pest-plugin-laravel: ^2.0 || ^3.0 || ^4.0
- phiki/phiki: ^2.2
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^1.0 || ^2.0
- phpstan/phpstan-phpunit: ^1.0 || ^2.0
- spatie/laravel-ray: ^1.26
Suggests
- laravel/scout: Required to use the scout search engine (^10.0)
Provides
None
Conflicts
None
Replaces
None
README
Base package for the Wsmallnews Filament ecosystem: shared models, component factories, Livewire traits, registries (search, sitemap, feeds, SEO, composition, scheduled tasks, modules), money handling, and the sn-* frontend CSS layer.
Every other Wsmallnews package builds on this one. Package-specific conventions for AI agents live in resources/boost/guidelines/core.blade.php.
Requirements
- PHP 8.2+
- Filament 4 or 5 (see
composer.json)
Installation
composer require wsmallnews/support:^1.0 php artisan sn-support:install
sn-support:install publishes this package's config and migrations, publishes the third-party resources it manages (media-library, laravel-settings, laravel-activitylog), then offers to run migrate. Add --no-deps to publish only this package.
Individual publish tags:
php artisan vendor:publish --tag="sn-support-config" php artisan vendor:publish --tag="sn-support-migrations" php artisan vendor:publish --tag="sn-support-translations" php artisan vendor:publish --tag="sn-support-views"
Configuration
All keys live in config/sn-support.php:
| Key | Default | Purpose |
|---|---|---|
filesystem_disk |
null |
Disk for uploads; null falls back to Filament's default disk |
models.content |
Models\Content::class |
Swappable model class |
models.sms_log |
Models\SmsLog::class |
Swappable model class |
models.scheduled_task |
Models\ScheduledTask::class |
Swappable model class |
models.composition |
Models\Composition::class |
Swappable model class |
models.page |
Models\Page::class |
Swappable model class |
tenant_model |
null |
Tenant model; null disables tenancy |
currency |
CNY |
Site currency (ISO 4217), see Money |
action_components.table_record_actions.group |
true |
Wrap record actions in an ActionGroup |
action_components.table_record_actions.trigger |
icon_button |
icon_button, button or link |
action_components.table_record_actions.{icon,label,color,size,outlined,tooltip} |
null / false |
Trigger appearance |
action_components.table_toolbar_actions.group |
true |
Wrap bulk actions in a BulkActionGroup |
action_components.table_toolbar_actions.trigger |
button |
button, icon_button or link |
action_components.table_toolbar_actions.{icon,label,color,size,outlined,tooltip} |
null / false |
Trigger appearance |
form_components.upload.* |
see file | visibility, downloadable, openable, reorderable, append_files, max_files, max_size, image_preview_height, panel_layout |
form_components.editor.max_length |
null |
Editor character limit |
form_components.editor.file_attachment.* |
see file | visibility, max_size |
form_components.editor.markdown.toolbar_buttons |
see file | Markdown toolbar layout |
form_components.editor.richtext.{toolbar_buttons,floating_toolbars,text_colors} |
see file | Rich text toolbar layout |
form_components.content.types |
null |
Allowed content types; null means all |
form_components.content.default_type |
ContentType::Richtext |
Default content type |
theme.* |
null |
Runtime overrides for --sn-* design tokens |
search.* |
see Search | Global search defaults |
sitemap.* |
see Sitemap | Sitemap and robots endpoints |
feeds.* |
see Feed | RSS endpoints |
scheduler.* |
see Scheduled tasks | Task runner settings |
Features
Base model layer
- SupportModel — base class for every package model, with
Scopeableand tenant awareness. - Scopeable —
scopeType(),scopeId(),scopeable(),snScope()query scopes. - Content — polymorphic content storage for
textarea,richtext,markdownandimages. - Composition, Page, Tag, ScheduledTask, SmsLog, Activity models.
Filament component factories
| Factory | Description |
|---|---|
FormComponents |
Upload, editor, content type group, enum toggles and order column inputs |
FilterComponents |
Status, morph and date range filters |
ActionComponents |
Delete, edit, toggle, switch and error-isolating bulk actions |
ColumnComponents |
Content, model, relation and morph table columns |
Factories apply config('sn-support.*') defaults. When you use a factory, do not set disk, visibility or similar defaults by hand.
Livewire traits
| Trait | Description |
|---|---|
HasProperties |
getProperty() / setProperty() over a property bag |
HasContentType |
Content type property (textarea, richtext, markdown, images) |
HasAuth |
Auth user management, authUser() / hasAuthUser() |
CanPagination |
Scroll, paginator and manual modes; includes WithPagination |
CanBeContained |
Whether the component renders its own container |
Scopeable |
#[Locked] scope type and id |
HasModuleContext |
Config lookup for a component reused by another module |
Eloquent casts
| Cast | Description |
|---|---|
CounterCast |
JSON counters with 0 defaults for missing keys |
MoneyCast |
Integer minor units to Cknow\Money\Money |
ImplodeCast |
Comma separated string to array |
Other capabilities
- Enums —
ContentType,CompositionStatus,PageStatus,ScheduledTaskStatus,ActivityLogEvent. - SMS —
Features\Smson top ofovertrue/easy-sms. - Tenant settings — a database settings repository for
spatie/laravel-settings. - Observers — activity log and media library observers.
- Middleware —
IdentifyTenantandInitializeSeo.
Usage
SupportModel
use Wsmallnews\Support\Models\SupportModel; class Post extends SupportModel { protected $table = 'posts'; } Post::scopeable('blog', 1)->get(); // scope filter Post::snScope('blog', 1)->get(); // scope plus current tenant $post->getScopeType(); // 'blog' $post->getScopeId(); // 1 $post->getScopeable(); // ['scope_type' => 'blog', 'scope_id' => 1]
scope_id may be 0, which means "every record of this scope type". Because 0 is a real value, queries must use scopeScopeId(0) (internally whereIn) rather than where('scope_id', 0).
Component factories
use Wsmallnews\Support\Filament\Forms\FormComponents; FormComponents::mediaImageUpload('avatar', 'avatars'); // Spatie Media Library FormComponents::mediaFileUpload('attachment', 'files'); FormComponents::plainImageUpload('cover'); // stored on the disk, path in the column FormComponents::plainFileUpload('document'); FormComponents::markdownEditor('description'); FormComponents::richEditor('content'); FormComponents::enumsToggleButtons(PostStatus::class, 'status'); FormComponents::orderColumnInput(); FormComponents::contentTypeGroup(types: null, defaultType: ContentType::Richtext);
use Wsmallnews\Support\Filament\Filters\FilterComponents; use Wsmallnews\Support\Filament\Actions\ActionComponents; use Wsmallnews\Support\Filament\Tables\ColumnComponents; FilterComponents::statusFilter(PostStatus::class); FilterComponents::createUpdateRangeFilter(); FilterComponents::dateTimeRangeFilter('published_at', 'Published'); ActionComponents::toggleAction(PostStatus::class, 'status'); // enum with exactly two cases ActionComponents::switchAction(PostStatus::class, 'status'); // enum with more than two cases ActionComponents::bulkAction('bulk_publish', fn ($action, $record) => $record->publish()); ColumnComponents::contentColumn('content', 'Content'); ColumnComponents::morphColumn('causer_type', 'Causer', fn ($r) => $r->causer, fn ($r) => $r->causer_type, fn ($r) => $r->causer_id);
bulkAction() wraps the per-record closure in its own try/catch, counts failures and only reports the first exception. Pass a prepare closure for batch preflight work such as eager loading; a throw there fails the whole batch.
Livewire
use Wsmallnews\Support\Livewire\Base; use Wsmallnews\Support\Livewire\Concerns\CanPagination; use Wsmallnews\Support\Livewire\Concerns\HasProperties; class CommentList extends Base { use CanPagination; use HasProperties; public string $pageType = 'scroll'; // scroll, paginator or manual public int $perPage = 10; public function loadComments(): void { $this->comments = $this->withPagination( Comment::query(), fingerprint: implode('|', [$this->search, $this->sort]), ); } }
Base::transaction() respects Halt:
$this->transaction(function (): void { // ... }); $this->halt(); // stop and roll back
withPagination() caches by page and fingerprint, so pass a fingerprint that covers every condition affecting the result set. Call resetPagination() after data changes.
Scopeable
use Wsmallnews\Support\Data\ScopeableContext; $context = new ScopeableContext('post', 0); $context->isGlobal(); // true ScopeableContext::fromArray(['scope_type' => 'store', 'scope_id' => 5]); scopeable_context(['scope_type' => 'post', 'scope_id' => 0]); scopeable_query(Post::query(), ['scope_type' => 'post', 'scope_id' => 0]);
Filament side:
Filament\Concerns\HasScopeableProperties— staticscopeType()/scopeId().Filament\Resources\Concerns\Scopeable—applyScopeableToQuery().Filament\Pages\Concerns\Scopeable— page-level scope helpers.
A misconfigured scope throws InvalidScopeException instead of falling back to a default instance.
Configuration mode for resources and pages
use Wsmallnews\Support\Filament\Concerns\CanBeConfigured; use Wsmallnews\Support\Filament\Resources\ResourceConfiguration; final class PostResource extends BaseResource { use CanBeConfigured; protected static ?string $configurationClass = ResourceConfiguration::class; }
use Wsmallnews\Support\Filament\Concerns\RegistersConfigurable; class CmsPlugin implements Plugin { use RegistersConfigurable; public function register(Panel $panel): void { $this->registerConfigurableResources($panel); $this->registerConfigurablePages($panel); } }
module_id is injected by the registering plugin, so cross-module registration needs no manual declaration.
Exceptions
Package exceptions extend SupportException, so callers can catch the whole family:
use Wsmallnews\Support\Exceptions\SupportException; class CmsException extends SupportException {}
InvalidScopeException signals a broken scope configuration.
Enum helper
use Wsmallnews\Support\Enums\Traits\EnumHelper; enum PostStatus: string implements HasColor, HasIcon, HasLabel { use EnumHelper; case Draft = 'draft'; case Published = 'published'; }
Casts
use Wsmallnews\Support\Casts\CounterCast; use Wsmallnews\Support\Casts\ImplodeCast; use Wsmallnews\Support\Casts\MoneyCast; protected $casts = [ 'counter' => CounterCast::class, // missing keys read as 0 'tags' => ImplodeCast::class, // comma separated string <-> array 'price' => MoneyCast::class . ':currency', // binds the row's currency column ]; $post->counter->like_num; // 0 $post->incrementJson('counter->like_num');
Rocket
Rocket is a three-stage pipeline payload: params (read-only input), radars (mutable intermediates) and payloads (final results).
$rocket = new Rocket; $rocket->setParams(['user_id' => 1]); $rocket->setRadar('key', 'value'); $rocket->setPayloads(['result' => $data]); $rocket->getParam('user_id'); $rocket->getRadar('key'); $rocket->getPayloads(); // Collection
Never serialise a whole Rocket: radars may hold Eloquent models. Pass scalars such as an id through queues instead.
Feature registries
All registries key their entries by module id (the plugin id, e.g. sn-cms). Register in packageBooted().
Modules
ModuleRegistry is the single source of truth for module identity. Register once per package:
use Wsmallnews\Support\Features\Modules\Module; use Wsmallnews\Support\Features\Modules\ModuleRegistry; ModuleRegistry::register(new Module( id: 'sn-cms', namespace: 'Wsmallnews\\Cms', plugin: CmsPlugin::class, ));
moduleOf($class)resolves the owning module by longest namespace prefix.HasModuleContextuses it as the default, so components need no manual owner.require($id)throws when a module is missing, which catches typos in registries at boot instead of silently dropping registrations.ModuleRegistry::flush()resets the registry in tests.
Search
SearchRegistry (facade Search) collects searchable sources per module.
use Wsmallnews\Support\Facades\Search; Search::config('sn-cms', [ 'display' => 'page', 'page' => fn (?string $query) => route('cms.search', ['q' => $query]), ])->registers('sn-cms', [ [ 'key' => 'post', 'model' => Post::class, 'group' => 'Posts', 'query' => fn ($query) => $query->published(), 'scopeable' => ['scope_type' => 'sn-cms', 'scope_id' => 0], 'url' => fn ($record) => route('cms.posts.show', $record), ], ]); Search::search(null, 'keyword'); // every registered module, grouped by label Search::search('sn-cms', 'keyword'); // one module; an unknown module throws
Source options:
| Option | Description |
|---|---|
key |
Source identifier; defaults to the morph alias or class basename |
model |
Model class or morph alias (required) |
group |
Group label; defaults to the model label |
fields |
LIKE columns; defaults to the model's keyword fields minus dotted relation paths |
limit |
Results per source |
sort |
Source order, ascending |
query |
LIKE engine query modifier |
scout |
Scout index modifier; overrides query, scopeable and fields |
scopeable |
Scope filter for the LIKE engine |
title / description / cover / badge |
Display resolvers; default to HasSnSubject data |
url |
Link resolver; only the registrant can provide it, otherwise items are unlinked |
view |
Custom item view; falls back to SearchSource::DEFAULT_ITEM_VIEW |
render |
fn ($result, $query) renderer, wins over view |
visible |
Closure deciding whether the source takes part |
results |
Fully custom results, bypassing the engine |
Module options (Search::config()) accept any key from sn-support.search. Resolution order everywhere is component property → module declaration → global config. Passing null restores the global fallback. Re-registering a key overrides the previous source, which lets an app replace a package source.
Engines: database (default, WHERE ... LIKE), scout (requires laravel/scout and Searchable on the model), or a class implementing Engines\Engine. Engines implementing ConfigurableEngine receive the module options.
Search config keys:
| Key | Default | Purpose |
|---|---|---|
engine |
database |
Fallback engine |
display |
dropdown |
dropdown or page |
page |
null |
Results page: string gets ?q= appended, closure builds the URL |
results_limit |
8 |
Results per source |
split_terms |
true |
Split the query on whitespace |
terms_operator |
and |
and or or for the split terms |
show_search_button |
false |
Render a submit button (page mode only) |
case_insensitive |
true |
Case-insensitive LIKE |
debounce |
300ms |
Frontend input debounce |
Components:
<livewire:sn-support::components.search :module="'sn-cms'" placeholder="Search…" :limit="5" /> <livewire:sn-support::components.search-results module="sn-cms" :limit="10" />
components.search renders a dropdown or a search field; components.search-results renders the result area of a dedicated page and keeps ?q= in sync.
Sitemap
SitemapRegistry (facade Sitemap) aggregates URL sources and robots rules. support exposes /sitemap.xml and /robots.txt at the root, so they work on every bound domain.
use Wsmallnews\Support\Facades\Sitemap; Sitemap::config('sn-cms', ['domain' => 'cms.example.com']) ->registers('sn-cms', [ [ 'key' => 'posts', 'urls' => fn () => Post::query()->published()->get()->map(fn ($post) => [ 'loc' => route('cms.posts.show', $post), 'lastmod' => $post->updated_at, ]), ], ]) ->robots('sn-cms', ['disallow' => ['cms/search']]);
robots.txtalways contains the Filament panel paths, the registered disallow rules and aSitemap:line for the current host.- Modules declaring a
domainthat does not match the request are skipped, including their robots rules. Route patterns such as{tenant:slug}.example.comare supported. - The rendered sitemap is cached as a whole; call
Sitemap::flush()after content changes.
Config keys: enabled, sitemap_uri, robots_uri, cache_ttl (null or 0 disables), domain_filter.
Feed
FeedRegistry (facade Feed) publishes RSS 2.0 streams. Unlike the sitemap, a site can expose several named streams, so readers subscribe to what they care about.
use Wsmallnews\Support\Facades\Feed; Feed::config('sn-cms', ['domain' => 'cms.example.com']) ->register('sn-cms', 'posts', [ 'title' => fn () => config('app.name') . ' - Posts', 'label' => fn () => __('Posts'), 'description' => fn () => null, 'link' => fn () => url('/'), 'limit' => 50, 'items' => fn () => Post::query()->published()->get()->map(fn ($post) => [ 'title' => $post->title, 'url' => route('cms.posts.show', $post), 'description' => $post->description, 'updated_at' => $post->published_at, ]), ]);
itemsis required and must be a closure; every entry needs aurl.- Feed names are URL segments and must be globally unique; duplicates throw at registration.
/feedmerges every visible stream, newest first, truncated byfeeds.limit;/feed/{name}serves one stream.- Register module-scoped endpoints inside the module route group with
Feed::routes('sn-cms'), which adds{prefix}/feedand{prefix}/feed/{name}. Call it before catch-all segments such asnavigation/{slug}. Feed::autodiscoveryTags($module)returns the<link rel="alternate">tags for the layout head.- Call
Feed::flush()after content changes.
Config keys: enabled, uri, limit, cache_ttl, domain_filter.
Seo
Seo holds per-module site defaults and the current page context.
use Wsmallnews\Support\Facades\Seo; // Site defaults, once per module in packageBooted() Seo::config('sn-cms', [ 'site_name' => fn () => config('app.name'), 'description' => fn () => null, 'image' => fn () => null, 'favicon' => fn () => '/favicon.ico', 'analytics_code' => fn () => null, ]); // Per page, in the routed page Seo::init('sn-cms') ->title($post->title) ->description($post->description) ->article(['datePublished' => $post->published_at?->toIso8601String()]);
Render in the layout: @snSeo in <head>, @snSeoAnalytics before </body>, @snFeeds('sn-cms') for autodiscovery links.
init()runs in theseo-init:{module}route middleware on first render only. Livewire update requests do not re-render the layout, so they need no re-initialisation.article()assembles an Article JSON-LD node from the page data and switchesog:typetoarticle;website()emits the WebSite node;jsonLd()appends raw nodes.- Relative image and canonical URLs are made absolute at render time.
Composition
CompositionRegistry stores the component types a module can compose, plus optional purpose slots (for example a detail-page sidebar).
use Wsmallnews\Support\Facades\CompositionRegistry; CompositionRegistry::registerPurposes('sn-cms', [ 'post-sidebar' => [ 'label' => fn () => __('Post sidebar'), 'positions' => ['left', 'right'], ], ])->registers('sn-cms', [ [ 'type' => 'post-detail', 'label' => fn () => __('Post detail'), 'component' => \Wsmallnews\Cms\Livewire\PostDetail::class, 'forms' => fn () => [/* Filament fields */], 'provides' => fn (array $extras) => ['post' => $extras['post'] ?? null], ], ]);
component/componentsmap one type to one or more Livewire components;formsreturns the admin form.providesseeds the row context for later components;contextdeclares which keys an item consumes. Context flows left to right inside a row and never crosses rows.- Rows resolve through
CompositionRenderer::resolveRows();resolveForPurpose()returnsnullwhen nothing matches so callers can fall back to their default layout.
Money
Store money as integer minor units in the database, never as floats.
sn_money()returns theMoneyManager:money(),minor(),decimal(),add(),subtract(),multiply(),sum(),allocate(),format(),formatParts(),symbol(),jsonFields(),jsonSum(),jsonFormat().- Scalar input is read as decimal major units;
intinput is read as minor units. Never pass a float result of your own arithmetic — go through the manager. - Split amounts with
allocate(); it distributes the remainder so the total is preserved. - Currency resolution:
config('app.currency')→sn-support.currency→CNY. MoneyCast::class . ':currency'binds a per-row currency column; plainMoneyCast::classalways uses the site currency.
sn_money()->format(1999); // ¥19.99 sn_money()->minor('19.99'); // 1999 sn_money()->allocate(10000, [1, 1, 1]); // [3334, 3333, 3333] sn_money()->jsonSum(['shipping' => 500, 'tax' => 120]);
Features\Currency and sn_currency() are deprecated; MoneyManager replaces them.
Scheduled tasks
ScheduledTaskRegistry (facade ScheduledTask) maps a morph type to the actions that can be scheduled for it.
use Wsmallnews\Support\Facades\ScheduledTask; ScheduledTask::registers('sn_post', [ 'publish' => ['label' => __('Publish'), 'handler' => PublishHandler::class], 'unpublish' => ['label' => __('Unpublish'), 'handler' => UnpublishHandler::class], ]); // In a form ScheduledTask::scheduleRepeater('sn_post');
Action options: action, label, forms (extra payload fields), handler (a closure receiving the task and payload), visible (closure receiving the repeater Get, used for mutually exclusive states). Actions without forms hide the payload fieldset.
The repeater only loads pending tasks; executed, cancelled and failed rows stay in the table for auditing.
Run due tasks with:
php artisan sn-support:run-scheduled-tasks
Schedule that command in routes/console.php or the console kernel. Config keys: scheduler.enabled, scheduler.frequency (everyMinute, everyFiveMinutes, dailyAt:13:00, ...), scheduler.batch_size, scheduler.without_overlapping, scheduler.overlapping_expire_minutes.
Filament resources:
Filament\Resources\ScheduledTasks\BaseResource— list and view pages, table and infolist.ScheduledTasks\Widgets\ScheduledTasks— footer or header widget for a detail page.ScheduledTasks\Concerns\ViewScheduledTasksAction— row action opening the record's tasks in a slide-over.
Activity logs and tags
Filament\Resources\ActivityLogs\BaseResource— list, view, export and timeline.getEloquentQuery()filters by the defaultlog_name. Models may implementContracts\HasModelLabelto customise their label in the type filter.Filament\Resources\Tags\BaseResource— extend it and overridegetTagType()to scope a tag resource to one tag type.
Multi-tenancy
Set sn-support.tenant_model to enable. IdentifyTenant is registered as a global Livewire persistent middleware and resolves the tenant from the route's tenant parameter by slug.
has_tenancy(); // whether a tenant is active current_tenant(); // panel or frontend tenant model frontend_has_tenancy(); frontend_current_tenant(); is_in_panel(); sn_route('page.show'); // adds the tenant parameter when needed
Tenant settings use Tenant\Settings\Repositories\DatabaseSettingsRepository with the sn_team_settings table. Register it in config/settings.php:
'repositories' => [ 'team_database' => [ 'type' => Wsmallnews\Support\Tenant\Settings\Repositories\DatabaseSettingsRepository::class, 'model' => null, 'table' => 'sn_team_settings', 'connection' => null, ], ],
For activity log, point activity_model at Wsmallnews\Support\Models\Activity. For media library, point media_observer at Wsmallnews\Support\Observers\MediaLibrary\MediaObserver; the default observer deletes files along with the media record, which breaks order items that still reference them.
Install commands
PackageInstallCommand is the base command for extension packages:
use Wsmallnews\Support\Commands\PackageInstallCommand; class CmsInstallCommand extends PackageInstallCommand { protected string $packageName = 'sn-cms'; protected function afterPublish(): void { // package-specific publish steps } }
What it does:
InstallPlannerbuilds the dependency plan by reading thewsmallnews/*requires of the target package and its dependencies fromvendor, then ordering them topologically. Only packages exposing an{sn-xxx}:installcommand are planned.- Dependencies are installed silently (
--as-dependency): publish only, no prompts. - The package publishes its own config and migrations, then runs
afterPublish(). - A single
migrateprompt runs every pending migration. - Only the package being installed is asked to star its repository.
Options: --no-deps skips dependency installation; --as-dependency is internal and performs no interaction.
Frontend styles
Include resources/css/index.css from your app stylesheet:
tokens.css— design tokens; the only entry point for theming. Geometric tokens flip atlg, colour tokens in.dark.utilities.css— sharedsn-*utilities (containers, badges, buttons, text, empty states, skeletons).content.css—sn-prosetypography for rendered content.filament.css—sn-filament-*overrides that depend on Filament internals, available only to packages that inherit support.
Conventions:
- Compose a base class (shape and size) with a modifier class (colour or state), e.g.
sn-badge sn-badge-sm sn-badge-danger. - Use
sn-gap,sn-marginandsn-paddedfor block-level spacing instead of fixedgap-4/p-4. sn-pageis the page skeleton,sn-contentthe content column,sn-page-xthe horizontal safe padding inside full-width bands.sn-containeris a content card (background, border, shadow), not a generic layout wrapper.sn-hover/sn-link/sn-activeonly style descendants when the outer element is asn-container.- For dynamic colours use
sn_badge_color()/sn_text_color(), which return['class' => ..., 'style' => ...].
Utility API
| Method | Purpose |
|---|---|
Utils::getConfig($key, $default) |
Read sn-support config with dot notation |
Utils::getModel($name, $throw = true) |
Resolve a configured model class; false allows null |
Utils::getContentModel() / getCompositionModel() / getPageModel() / getScheduledTaskModel() / getSmsLogModel() |
Configured model classes |
Utils::getFilesystemDisk() |
Upload disk, falling back to Filament's default |
Utils::getScopeFromInstances($configKey, $key = null) |
Resolve a ScopeableContext from a module's scopeables |
Utils::isTenancyEnabled() / getTenantModel() |
Tenancy state and model |
generate_slug() |
Slug with word-boundary truncation and a random fallback |
text_highlight($text, $query) |
Escaped keyword highlighting with <mark> |
sn_badge_color() / sn_text_color() |
Semantic colour to class and style |
href_format($url, $newTab, $spaMode) |
href / target / wire:navigate attributes |
files_url($files, $disk) |
File URL resolution |
exception_log($e, $name, $message) |
Structured exception logging |
through_cache($key, $callback, $store, $force, $ttl) |
Cache-through reads |
tree_to_flatten($tree) |
Flatten a tree collection |
remove_query_param_from_url($url, $keys) |
Strip query parameters |
scopeable_context() / scopeable_query() |
Build and apply scopes |
Testing
composer test
Changelog
See CHANGELOG.
Contributing
See CONTRIBUTING.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
- smallnews
- bezhansalleh/filament-plugin-essentials
- filament/filament
- overtrue/easy-sms
- ralphjsmit/livewire-urls
- spatie/laravel-activitylog
- spatie/laravel-markdown
- spatie/laravel-medialibrary
- spatie/laravel-settings
- All Contributors
License
The MIT License (MIT). See License File.