Skip to content

Next-gen: javaabu/widgets v1.0 #12

Open

Description

@n4ff4h

WordPress classic-style widgets: widgets define their settings with stencil and how they render; developers define widget locations; Blade and Inertia React; optional translatable.

Package: javaabu/widgets · Target: v1.0 · Wave: 4
Depends on: javaabu/stencil, javaabu/permissions, javaabu/manifest
Optional: javaabu/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.

Checklist

Developed by an agent orchestrated from the Javaabu next-gen plan. Cross-package requests go in the dependency's repo with the cross-package label.

Activity

  1. added
    next-genNext-gen Javaabu packages
    epicTracks a whole package
    on Nov 4, 2026
  2. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    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'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.

    • 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', ...]).

      • 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.

    • 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 the javaabu-widgets skill.

    Public API sketch

    class RecentPostsWidget extends Widget
    {
        public static function key(): string { return 'recent-posts'; }
        public function name(): string { return __('Recent posts'); }
    
        public function settings(): array
        {
            return [
                TextInput::make('title')->translatable(),
                TextInput::make('count')->integer()->default(5)->required(),
            ];
        }
    
        public function data(WidgetInstance $widget): array
        {
            return ['posts' => Post::latest()->limit($widget->setting('count'))->get(['id', 'title'])];
        }
    }
    
    // AppServiceProvider::boot()
    Widgets::register(RecentPostsWidget::class);
    Widgets::location('sidebar', __('Sidebar'))->max(6);
    Widgets::location('footer', __('Footer'))->allow([TextWidget::class, LinkWidget::class]);
    
    // routes/web.php
    Route::widgets(['guard' => 'web_admin', 'prefix' => 'admin/widgets', 'name' => 'admin.widgets']);
    <x-widgets::location name="sidebar" />
    <WidgetLocation widgets={widgets.sidebar} components={{ "recent-posts": RecentPosts }} />

    Order of work

    1. Foundations: PHP 8.4 / Laravel 12+, CI matrix, coverage and codestyle #1 foundations: composer, Pint, CI matrix including a leg without optional packages, the helpers check, and JS tooling.
    2. Widget definitions #2 definitions and Widget locations #3 locations, then post the API here for javaabu/cms.
    3. Overridable controllers, policies and default permissions (RBAC, multi-auth) #9 routes, controllers and policy. Widget management UI #4 and Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #8 management UI in Blade and React, with drag and drop.
    4. Optional javaabu/translatable support #10 translatable, then Dhivehi localization and RTL #7 Dhivehi/RTL.
    5. Documentation for people and agents #5 docs and Laravel Boost guideline and skill #6 Boost.

    Decisions and open questions

    • Moving between locations: done with a location field on the edit form, not by dragging across lists. This keeps keyboard and screen-reader use simple.
    • No inactive-widgets area and no per-page visibility rules in v1. Removing a widget deletes it.
    • No widget for raw HTML among the built-ins. It would need sanitising and a separate permission; apps can add one.
  3. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Widget and location API is on main (for javaabu/cms and apps)

    Require it like this:

    "repositories": [{ "type": "vcs", "url": "https://github.com/Javaabu/widgets" }],
    "require": { "javaabu/widgets": "^1.0@dev" }

    You'll also need the VCS repositories for forms, manifest, permissions and stencil, as in this repo's composer.json.

    A widget type

    use Javaabu\Stencil\Fields\TextInput;
    use Javaabu\Widgets\Models\WidgetInstance;
    use Javaabu\Widgets\Widget;
    
    class RecentPostsWidget extends Widget
    {
        public static function key(): string { return 'recent-posts'; }   // stored on each instance
        public function name(): string { return __('Recent posts'); }
        public function description(): ?string { return __('The latest posts.'); }
        public function icon(): ?string { return 'newspaper'; }           // Lucide name, for the admin page
    
        public function settings(): array                                  // stencil components = the settings form
        {
            return [
                TextInput::make('title')->label(__('Title'))->translatable(),
                TextInput::make('count')->label(__('Number of posts'))->integer()->default(5)->required(),
            ];
        }
    
        public function data(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():

    use Javaabu\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 renders
    Widgets::widgets('sidebar');        // Collection<WidgetInstance>, ordered (loaded once per request; optional cache)
    Widgets::render('sidebar');         // HtmlString
    Widgets::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
    <x-widgets::location name="sidebar" class="space-y-6" />
    import { WidgetLocation, registerWidgets } from "@javaabu/widgets"   // through javaabu/manifest
    
    registerWidgets({ "recent-posts": RecentPosts })   // ({ widget, settings, data }) => JSX
    <WidgetLocation name="sidebar" widgets={widgets.sidebar} />

    Admin pages

    Route::widgets(['guard' => 'web_admin', 'prefix' => 'admin/widgets', 'name' => 'admin.widgets', 'middleware' => [...], 'controller' => MyController::class]);
    • 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.
  4. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Final summary

    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).

    The API for javaabu/cms is in this comment.

    Intentionally out of scope for v1

    • 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.
    • Docs registration. Register with docs.javaabu.com (Register with Javaabu Docs (docs.javaabu.com) #11).
    • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicTracks a whole packagenext-genNext-gen Javaabu packages

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions