You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WordPress classic-style widgets: widgets define their settings with stencil and how they render; developers define widget locations; Blade and Inertia React; optional translatable.
Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.
New repo, so all work goes on main. It's built from package-skeleton conventions, with javaabu/settings v2 and javaabu/block-editor as the in-house references.
Architecture
Widget definitions (Widget definitions #2). A widget is a class extending Javaabu\Widgets\Widget, similar to WordPress's WP_Widget. It defines:
key(), name(), description() and icon()
settings(): array: stencil components, which make up the configuration form
data(WidgetInstance $widget): array: extra server-side data for rendering, such as a query
view(): a Blade view, widgets::widgets.{key} by default
component(): the React component name, the studly key by default
Types are registered through the Widgets facade (Widgets::register(MyWidget::class)) or config('widgets.widgets'). Built-in types are Text (title + body), Image (image URL, alt, link) and Link (a call-to-action button). javaabu/cms adds more.
Locations (Widget locations #3).Widgets::location('sidebar', __('Sidebar')) returns a WidgetLocation. It has ->description(), ->allow([...types])/->deny(), ->max(5), ->permission('edit_footer_widgets') and ->wrapper(view). Locations can also be set in config('widgets.locations').
Blade renders a location with <x-widgets::location name="sidebar" /> (or Widgets::render('sidebar')), and Widgets::has('sidebar') tells you whether it has any widgets.
React gets serialised widgets from Widgets::toArray(['sidebar', 'footer']), usually as a shared Inertia prop, and renders them with <WidgetLocation widgets={...} />. A component registry maps widget types to React components: the built-ins are already in it, and apps add theirs with registerWidgets({...}) or the components prop.
Blade and React produce the same markup for the built-in widgets, checked by shared fixtures.
Storage. A widget_instances table holds id, location, type, settings (JSON), order and timestamps. The model is Models\WidgetInstance, swappable from config. Widgets are loaded once per request per location, and there's an optional cache that is cleared when a widget is saved.
Routes: index of locations, create/store (stencil settings form), edit/update, destroy, and PUT {location}/order for ordering.
Moving widgets: the edit form has a location select to move a widget to another location.
Drag and drop: reordering is keyboard accessible. You pick up an item with the handle button (Space), move it with the arrow keys, and drop it with Space (Escape cancels). An aria-live region announces each move, and the order saves on drop. Blade uses a small vanilla TS script, shipped prebuilt in dist/ and registered with javaabu/manifest; React uses its own component.
Stacks: Blade pages are built with javaabu/forms v2 components and stencil's Blade driver. Inertia pages are widgets::Widgets/Index and widgets::Widgets/Edit, built with @javaabu/forms, Onu and <StencilForm>. config('widgets.views') picks blade or inertia.
Overrides and auth: controllers can be overridden from config or with the controller route option. WidgetPolicy is registered with Permissions::policy(). The permissions are view_widgets, edit_widgets and delete_widgets, plus an optional per-location permission. Nothing assumes the web guard: tests run on two guards.
Translatable (Optional javaabu/translatable support #10). Setting fields declared ->translatable() are stored as locale maps inside settings. The settings form implements stencil's ResolvesTranslatableFields, so with javaabu/translatable installed these fields get one input per locale (title_en, title_dv). Rendering picks the current locale and falls back to the default. Without the package, they're plain values. This is feature-detected in one place (Support\Translations), and CI has a leg without translatable.
Dhivehi/RTL (Dhivehi localization and RTL #7). Strings live in lang/en.json and lang/dv.json, and Thaana is used in tests. Views use logical utilities only.
You'll also need the VCS repositories for forms, manifest, permissions and stencil, as in this repo's composer.json.
A widget type
useJavaabu\Stencil\Fields\TextInput;
useJavaabu\Widgets\Models\WidgetInstance;
useJavaabu\Widgets\Widget;
class RecentPostsWidget extends Widget
{
publicstaticfunctionkey(): string { return'recent-posts'; } // stored on each instancepublicfunctionname(): string { return__('Recent posts'); }
publicfunctiondescription(): ?string { return__('The latest posts.'); }
publicfunctionicon(): ?string { return'newspaper'; } // Lucide name, for the admin pagepublicfunctionsettings(): array// stencil components = the settings form
{
return [
TextInput::make('title')->label(__('Title'))->translatable(),
TextInput::make('count')->label(__('Number of posts'))->integer()->default(5)->required(),
];
}
publicfunctiondata(WidgetInstance$widget): array// extra props for Blade and React
{
return ['posts' => Post::latest()->limit($widget->setting('count'))->get(['id', 'title'])];
}
// Optional overrides:// view() defaults to 'widgets::widgets.{key}'; the view gets $widget, $settings (current locale) and data()// component() React component name, defaults to Str::studly(key)// shouldRender(WidgetInstance) to skip an instance; title(WidgetInstance) for the admin list
}
Registering types and locations
These calls go in a service provider's boot():
useJavaabu\Widgets\Facades\Widgets;
Widgets::register([RecentPostsWidget::class]);
Widgets::unregister(ImageWidget::class); // the built-ins are text, image and link (config('widgets.widgets'))Widgets::location('sidebar', fn () => __('Sidebar')) // label: a string or a closure, translated when shown
->description(fn () => __('Next to posts'))
->allow([TextWidget::class, 'recent-posts']) // or ->deny([...]); classes or keys
->max(6)
->permission('edit_sidebar_widgets') // replaces edit_widgets for this location; registered with javaabu/permissions
->wrapper('theme.widget') // Blade wrapper per widget (default config('widgets.wrapper'))
->order(10); // position on the admin page
Locations can also be set in config('widgets.locations') as key => label.
Reading and rendering
Widgets::has('sidebar'); // bool: any widget that rendersWidgets::widgets('sidebar'); // Collection<WidgetInstance>, ordered (loaded once per request; optional cache)Widgets::render('sidebar'); // HtmlStringWidgets::toArray('sidebar'); // for React: [{ id, type, component, location, settings, data }]Widgets::props(['sidebar', 'footer']); // keyed by location, e.g. Inertia::share('widgets', fn () => Widgets::props())$widget->setting('title'); // current locale for translatable settings, falls back to the default locale
Routes:index, create (?type=), store, order (PUT {location}/order, widgets[]), edit, update (_location moves the widget), destroy. JSON is returned when the request expects it.
Pages: set config('widgets.views') to blade or inertia. The Inertia pages are widgets::Widgets/Index and widgets::Widgets/Edit.
Permissions:view_widgets, edit_widgets and per-location permissions. WidgetPolicy covers WidgetLocation and WidgetInstance; override it with config('widgets.policy') or Gate::policy().
Model:config('widgets.model'), table widget_instances. The migration loads automatically; call Widgets::ignoreMigrations() to turn that off.
javaabu/widgets v1.0 is on main, and CI is green on every job:
PHP 8.4/8.5 × Laravel 12/13 × lowest/stable
Pint
a leg without Inertia and translatable
the JS job: lint, format, typecheck, Vitest with coverage, an examples build and a dist freshness check
There are 92 PHPUnit tests (one is skipped unless Inertia is missing) and 27 Vitest tests. Every next-gen issue is closed except #11 (needs a person).
What was built
Widget types. A widget type is a class extending Widget. Its settings are javaabu/stencil components, it renders with a Blade view and a React component, and it can add data() and shouldRender().
Built-in widgets are Text, Image and Link. Blade and React render the same markup, which shared fixtures check from both sides.
Links go through a SafeUrl rule and are checked again at render time, in PHP and TypeScript.
Locations.
Defined with Widgets::location(): allow/deny, max, a permission of their own, a wrapper and an order. They can also be set in config.
Rendered with <x-widgets::location> in Blade and <WidgetLocation> in React, using Widgets::props(). React components for widget types are added with registerWidgets().
Widgets are loaded once per request per location, with an optional versioned cache.
Admin pages.Route::widgets() gives Blade (javaabu/forms v2) or Inertia React (@javaabu/forms, Onu, <StencilForm>) pages, and every route also answers in JSON.
Admins can add, configure, move (with a Location select), remove and reorder widgets.
Reordering works by dragging a handle or by keyboard: Space, the arrow keys/Home/End, Space, with Escape to cancel. Each step is announced to screen readers.
Blade uses createSortable (a prebuilt widgets::admin script in dist/), and React uses SortableList.
Permissions.view_widgets, edit_widgets and per-location permissions, registered through javaabu/permissions.
WidgetPolicy covers locations and widgets and can be overridden from config or Gate::policy().
The controller can be overridden too. Nothing assumes the web guard; the tests use two guards.
Translatable settings (optional). With javaabu/translatable installed, settings marked ->translatable() get one input per language, through stencil's ResolvesTranslatableFields. They're stored as locale maps and render in the current locale, falling back to the default.
Dhivehi and RTL. Every string is in English and Dhivehi, the views and styles use logical properties, and the arrow keys follow the reading direction.
Docs. 20 Docusaurus pages, llms.txt, AGENTS.md/CLAUDE.md, the Boost guideline and the javaabu-widgets skill. There are runnable examples (pnpm examples).
No inactive-widgets area and no per-page visibility rules. Removing a widget deletes it. Apps can add visibility rules with shouldRender().
No raw-HTML widget among the built-ins. It would need sanitising and a separate permission.
Moving between locations is done from the edit form, not by dragging across lists. That keeps it simple with a keyboard or screen reader.
Image widgets take a URL, not an upload. A mediapicker-based image widget can come later; javaabu/cms or apps can register one.
The primary language is the editor's current locale. When translatable is on, that language is required and the others are optional; this follows stencil's translatable contract.
For the user
Codecov. Make sure CODECOV_TOKEN is available to this repo. Uploads use fail_ci_if_error: false, so CI stays green either way, but the badge stays blank without the token.
Dhivehi review. The terms listed in docs/advanced-usage/dhivehi-and-rtl.md#for-review need a Dhivehi speaker.
Visual check. The Blade and React admin pages are tested with PHPUnit and jsdom, but nobody has looked at them in a real browser inside an app layout yet.
Release. Tag v1.0 and submit to Packagist. Once forms v2, manifest, permissions v3, stencil and translatable v3 are released, drop the VCS repositories in composer.json. Once @javaabu/forms is on npm, change the npm dependency in src/Assets/Assets.php and package.json.
Activity
n4ff4h commentedon Nov 5, 2026
Implementation plan
New repo, so all work goes on
main. It's built from package-skeleton conventions, with javaabu/settings v2 and javaabu/block-editor as the in-house references.Architecture
Widget definitions (Widget definitions #2). A widget is a class extending
Javaabu\Widgets\Widget, similar to WordPress'sWP_Widget. It defines:key(),name(),description()andicon()settings(): array: stencil components, which make up the configuration formdata(WidgetInstance $widget): array: extra server-side data for rendering, such as a queryview(): a Blade view,widgets::widgets.{key}by defaultcomponent(): the React component name, the studly key by defaultTypes are registered through the
Widgetsfacade (Widgets::register(MyWidget::class)) orconfig('widgets.widgets'). Built-in types are Text (title + body), Image (image URL, alt, link) and Link (a call-to-action button). javaabu/cms adds more.Locations (Widget locations #3).
Widgets::location('sidebar', __('Sidebar'))returns aWidgetLocation. It has->description(),->allow([...types])/->deny(),->max(5),->permission('edit_footer_widgets')and->wrapper(view). Locations can also be set inconfig('widgets.locations').<x-widgets::location name="sidebar" />(orWidgets::render('sidebar')), andWidgets::has('sidebar')tells you whether it has any widgets.Widgets::toArray(['sidebar', 'footer']), usually as a shared Inertia prop, and renders them with<WidgetLocation widgets={...} />. A component registry maps widget types to React components: the built-ins are already in it, and apps add theirs withregisterWidgets({...})or thecomponentsprop.Storage. A
widget_instancestable holdsid,location,type,settings(JSON),orderand timestamps. The model isModels\WidgetInstance, swappable from config. Widgets are loaded once per request per location, and there's an optional cache that is cleared when a widget is saved.Management UI (Widget management UI #4, Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #8, Overridable controllers, policies and default permissions (RBAC, multi-auth) #9). Apps register the routes with
Route::widgets(['guard' => 'web_admin', 'prefix' => 'admin/widgets', ...]).PUT {location}/orderfor ordering.aria-liveregion announces each move, and the order saves on drop. Blade uses a small vanilla TS script, shipped prebuilt indist/and registered with javaabu/manifest; React uses its own component.widgets::Widgets/Indexandwidgets::Widgets/Edit, built with @javaabu/forms, Onu and<StencilForm>.config('widgets.views')picksbladeorinertia.controllerroute option.WidgetPolicyis registered withPermissions::policy(). The permissions areview_widgets,edit_widgetsanddelete_widgets, plus an optional per-location permission. Nothing assumes thewebguard: tests run on two guards.Translatable (Optional javaabu/translatable support #10). Setting fields declared
->translatable()are stored as locale maps insidesettings. The settings form implements stencil'sResolvesTranslatableFields, so with javaabu/translatable installed these fields get one input per locale (title_en,title_dv). Rendering picks the current locale and falls back to the default. Without the package, they're plain values. This is feature-detected in one place (Support\Translations), and CI has a leg without translatable.Dhivehi/RTL (Dhivehi localization and RTL #7). Strings live in
lang/en.jsonandlang/dv.json, and Thaana is used in tests. Views use logical utilities only.Docs/Boost (Documentation for people and agents #5, Laravel Boost guideline and skill #6). Docusaurus docs,
llms.txt,AGENTS.md/CLAUDE.md, the Boost guideline, and thejavaabu-widgetsskill.Public API sketch
Order of work
Decisions and open questions
n4ff4h commentedon Nov 5, 2026
Widget and location API is on
main(for javaabu/cms and apps)Require it like this:
You'll also need the VCS repositories for forms, manifest, permissions and stencil, as in this repo's
composer.json.A widget type
Registering types and locations
These calls go in a service provider's
boot():Locations can also be set in
config('widgets.locations')askey => label.Reading and rendering
Admin pages
index,create(?type=),store,order(PUT {location}/order,widgets[]),edit,update(_locationmoves the widget),destroy. JSON is returned when the request expects it.config('widgets.views')tobladeorinertia. The Inertia pages arewidgets::Widgets/Indexandwidgets::Widgets/Edit.view_widgets,edit_widgetsand per-location permissions.WidgetPolicycoversWidgetLocationandWidgetInstance; override it withconfig('widgets.policy')orGate::policy().config('widgets.model'), tablewidget_instances. The migration loads automatically; callWidgets::ignoreMigrations()to turn that off.n4ff4h commentedon Nov 5, 2026
Final summary
javaabu/widgets v1.0 is on
main, and CI is green on every job:distfreshness checkThere are 92 PHPUnit tests (one is skipped unless Inertia is missing) and 27 Vitest tests. Every
next-genissue is closed except #11 (needs a person).What was built
Widget. Its settings are javaabu/stencil components, it renders with a Blade view and a React component, and it can adddata()andshouldRender().SafeUrlrule and are checked again at render time, in PHP and TypeScript.Widgets::location(): allow/deny, max, a permission of their own, a wrapper and an order. They can also be set in config.<x-widgets::location>in Blade and<WidgetLocation>in React, usingWidgets::props(). React components for widget types are added withregisterWidgets().Route::widgets()gives Blade (javaabu/forms v2) or Inertia React (@javaabu/forms, Onu,<StencilForm>) pages, and every route also answers in JSON.createSortable(a prebuiltwidgets::adminscript indist/), and React usesSortableList.view_widgets,edit_widgetsand per-location permissions, registered through javaabu/permissions.WidgetPolicycovers locations and widgets and can be overridden from config orGate::policy().webguard; the tests use two guards.->translatable()get one input per language, through stencil'sResolvesTranslatableFields. They're stored as locale maps and render in the current locale, falling back to the default.llms.txt,AGENTS.md/CLAUDE.md, the Boost guideline and thejavaabu-widgetsskill. There are runnable examples (pnpm examples).The API for javaabu/cms is in this comment.
Intentionally out of scope for v1
shouldRender().For the user
CODECOV_TOKENis available to this repo. Uploads usefail_ci_if_error: false, so CI stays green either way, but the badge stays blank without the token.docs/advanced-usage/dhivehi-and-rtl.md#for-reviewneed a Dhivehi speaker.composer.json. Once @javaabu/forms is on npm, change the npm dependency insrc/Assets/Assets.phpandpackage.json.