Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Atrium Laravel Package

atriumphp/atrium

View on GitHub
Deep Wiki
Context7
0.2.0

This release adds the relations & nesting feature (relation managers + nested resources, REL-01..22) and record View pages (VIEW-*), a configurable record identifier field, the migration of panel icons to Symfony UX Icons, and makes path_prefix authoritative for route matching. Pre-1.0 — the public API may still change in a 0.x minor; API-affecting items are flagged below.

Added

  • Relation managers & nested resources — feature complete (REL-01..22). The relations & nesting feature is done across its five milestones: the shared table core + descriptor + read-only one-to-many manager; one-to-many owned create/edit/delete + associate/dissociate; many-to-many attach/detach + tabs; nested resources; and this close-out (extraction, playground, browser-verify, review). Dogfooded in the playground: Article → Comments (1:M, extracted via ->using()), Article ↔ Tags (M:N), Course → Lessons (nested).
  • Extractable relation config — ->using() (REL-19). A relation configures its table()/form() inline by default, or extracts them to a dedicated RelationManagerConfiguration subclass referenced by Relation::using(MyConfig::class) — the same inline-or-dedicated split pages() offers. The manager uses the class's table() for its columns and layers its form() over the target resource's form in the owned create/edit modal. (Behaviour change) wiring this also activates a relation's inline ->form() closure in the modal — previously stored but ignored; a resource that set Relation::form(...) will now see it applied. Atrium:Form gains optional relationResource/relationName mount arguments (additive; defaults preserve behaviour). See Relations › Extracting a relation.
  • Nested resources (REL-14..18, REL-20). A resource becomes nested under a parent record by declaring parent(): ?ParentRelationParentRelation::make(ParentResource::class)->relationship('tasks')->foreignKey('projectId')->recordTitle('name'), validated lazily against the registry (parent registered, the named one-to-many relation exists, foreign keys agree). An explicit nested route family (/{prefix}/{parentResource}/{parentId}/{resource}/… for index/new/{id}/{id}/edit, proven non-colliding with the flat routes) serves full CRUD scoped to the parent: the list filters by the parent foreign key, create presets it, and a child of another parent (a forged id) is a 404 (resolved against both the child's scopeQuery() and the parent FK). A breadcrumb renders the ancestry (Projects › Alpha › Tasks), and a parent-page relation manager whose target is nested links each row into the child's nested view/edit pages and points New at nested create instead of inline modals. (API) new AdminResource::parent() hook (default null = top-level) and the ParentRelation builder; Atrium\Action\ActionContext is no longer finalNestedActionContext extends it to emit 5-segment URLs (additive, not a break); Atrium\Page\PageContext gains optional parentResourceSlug / parentRecordId / parentRecords constructor arguments and a nestedUrl() helper (additive); Atrium:DataTable gains an optional parentId mount argument (additive) that scopes the list and emits nested URLs. See Nesting resources.
  • Relation managers — many-to-many + tabs (REL-02, REL-06, REL-07, REL-08, REL-12). A manyToMany() relation (declared with pivotTable() + pivotKeys() + optional pivotColumns()) renders a manager that Attaches an existing record (a listLinkable picker that excludes already-linked rows, plus a field per pivot column) and Detaches it (row + bulk; the pivot row is removed, both records persist). Pivot reads/writes go through the RelationDataProvider seam (Doctrine via DBAL — the pivot is never mapped as an entity; array adapter via an in-memory pivot store). When a resource declares several relations, the host renders a server-driven tab strip and mounts only the active relation's manager. (API) new default-allow hooks canAttach(object $parent, object $child) / canDetach(...) (override to restrict). Pivot-column display in the related table is a later enhancement. Owned create/edit/delete remain one-to-many only.
  • Relation managers — one-to-many actions (REL-05, REL-06, REL-07, REL-09, REL-12). A one-to-many RelationManager now offers a full lifecycle scoped to the parent: owned Create/Edit (an inline modal hosting the target resource's form — create sets the foreign key in one transaction), Delete + bulk delete, and Associate/Dissociate (link/unlink an existing record via a listLinkable-backed picker). Managers are read-only on the View screen (Relation::readOnlyOnView(), default true) and can be hidden per parent with Relation::visible(fn ($parent) => …). (API) new default-allow authorization hooks AdminResource::canAssociate(object $parent, object $child): bool and canDissociate(...) (one-to-many) and canAttach(...) / canDetach(...) (many-to-many) (override to restrict link/unlink); owned create/edit/delete continue to gate on the target resource's can(...). Atrium:Form gains optional embedded / presetValues / notifyEvent mount arguments (additive; defaults preserve behaviour) so it can be hosted inside a relation manager's modal. Owned row View navigation lands with nested resources (REL-M4).
  • Relations foundation (REL-01..04, REL-10, REL-11, REL-20). A resource declares managed relationships with relations() returning Relation::make(...) descriptors (one-to-many / many-to-many, explicit keys — no Doctrine in core). New storage-agnostic RelationDataProvider seam (Doctrine + array adapters; one-to-many read side) and an AbstractRecordTable core extracted from DataTable (no behaviour change). A read-only RelationManager Live Component renders a parent-scoped related table, embedded on the Edit page via a RelationManagers host. Link/unlink actions, many-to-many, and nested resources land in subsequent milestones. PRD: docs/PRDs/PRD-relations-nesting.md.
  • Configurable record identifier field. A resource can now address its records by a property other than id — a primary key named something else, or a natural key such as a slug for human-readable URLs — by overriding the new AdminResource::getIdentifierField(): string (defaults to 'id'). The field is used both to read a record's identifier (building its row/view/edit URLs) and to resolve a record back from a URL, honouring scopeQuery(). A UUID/ULID key named $id already worked and still needs no configuration. (API) DataProviderInterface::find() gains a trailing string $idField = 'id' parameter — backward-compatible for callers, but a signature change for any third-party provider implementation. The Doctrine adapter keeps the fast identity-map path for primary-key lookups and queries WHERE <field> = :id for a custom field; the array adapter matches on the given field.
  • Record View (read-only) pages — core (VIEW-01..04, VIEW-11, VIEW-13, VIEW-15, VIEW-18). A resource can expose a read-only View screen for one record at the bare URL /{resource}/{id}, opt-in by registering a 'view' page (Atrium\Page\ViewPage, with an Edit header link). Content is declared with AdminResource::view(Schema) — a schema of read-only entry components that live in the same layout tree as form fields; if a resource defines no view(), the screen falls back to its form() fields rendered read-only. Ships the Atrium\View\Entry base (the full shared configuration surface: labelling, layout/align, record-aware visible/hidden, state/getStateUsing/ formatStateUsing/default/placeholder, tooltip/helperText/hint, icon, url, inline actions) and Atrium\View\TextEntry (badge, colour, money, date/time, numeric, limit/words, prefix/suffix, html, lists, copyable). Reuses the existing view/canView($record) ability; adds PageContext::viewUrl(). The remaining entry types (Icon/Image/Color/KeyValue/Repeatable/Code), view-screen widget bands, ViewAction + clickable rows, and docs land in the following milestones. PRD: docs/PRDs/PRD-record-view.md.
  • Record View entry family (VIEW-05..08, VIEW-10). Five more read-only entries alongside TextEntry, each sharing the full base configuration surface: Atrium\View\IconEntry (value as an icon; boolean() true/false ticks, color(), size()), ImageEntry (image/avatar; circular()/square(), imageSize()/imageWidth()/imageHeight(), defaultImageUrl(), with URL-scheme sanitisation), ColorEntry (a sanitised colour swatch, copyable()), KeyValueEntry (a 1-D array/JSON map as a key→value table, keyLabel()/valueLabel()) and CodeEntry (a monospace, escaped code block; language(), copyable()). The semantic colour vocabulary is shared with Content\Text / Table\Column via a common ResolvesColor concern. Docs: docs/integration-guide/pages/view.md.
  • Record View — RepeatableEntry (VIEW-09). Repeats a nested entry schema once per item of a relation / array attribute (a Doctrine collection, an array of entities, or an array of maps), binding each item as the record for the nested entries — schema(), columns(), grid() and contained(). It re-enters the shared layout renderer, so nested containers and nested repeatables compose. This completes the entry family.
  • Record View — navigation, Delete and widget bands (VIEW-12, VIEW-16, VIEW-17). When a resource has a View screen, its list rows link to it by default (a stretched overlay link; the row's buttons stay clickable), with a configurable target — TableConfiguration::recordUrl() takes 'view' (default), 'edit', a fn (object $record): ?string, or null to disable. Adds Atrium\Table\Action\ViewAction (the bare-record link, gated by view) and ActionContext::recordRootUrl(). The View screen's header now runs its Delete server action through the shared confirm → delete plumbing (hosted by a small RecordActions Live Component, so the otherwise-static screen needs no client JS). ViewPage::headerWidgets()/footerWidgets() add record-scoped widget bands above/below the entries (the widget context gains recordId), reusing the list/dashboard widget mechanism. Docs: docs/integration-guide/pages/view.md.

Changed

  • Hardening from the full-solution code review. Several defense-in-depth fixes, none affecting a correctly-configured Doctrine install:

    • Record screens fail closed without a data provider. On a no-Doctrine install (DataProviderInterface absent), edit/view and their nested variants now return 404 instead of rendering form/view chrome that could neither load nor authorize its record. create/list (which need no record) are unaffected.
    • Form::save() authorizes before developer hooks. canCreate()/canEdit() now run before mutateFormDataBeforeValidate()/afterValidate(), so a forged Live-action POST can no longer trigger those side-effect hooks unauthorized.
    • Action URLs are scheme-guarded. A custom Action::url(fn …) result is now passed through the same javascript:/data:-rejecting guard as table row URLs and view entries before reaching an href.
    • Many-to-many adapter parity. The Doctrine attach() is now idempotent (no duplicate pivot row / unique-constraint 500), its pivot lookup quotes identifiers (reserved-word/portability safe), and the array adapter matches pivot ids by strict, type-coerced comparison — both adapters now agree.
    • TextEntry::lineClamp() works. The line-clamp-N utilities are now compiled into the shipped stylesheet (previously purged, so the class had no effect). Relation/confirmation modals and the relation tab strip gained aria-labelledby / aria-selected / role="tabpanel".
  • path_prefix is now authoritative for route matching, not just link generation. The parametric routes (config/routes.php) previously hardcoded /admin, while path_prefix only changed the URLs the panel generated — so setting it to anything else broke the panel (links pointed somewhere the routes did not match). The routes now mount under the %atrium.path_prefix% parameter, so a single config value relocates both matching and generation together. Setting path_prefix: '/administrator' now actually serves the panel there; the default stays /admin and the route import is unchanged. Subdomain hosting (admin.example.com) is documented via the route import's host: option. Docs: docs/integration-guide/panel/customization.md#changing-the-url-the-panel-lives-at.

  • Icons now render through Symfony UX Icons. The panel's hardcoded name → SVG path Twig map ([@Atrium](https://github.com/Atrium)/icon.html.twig, ~18 heroicons-outline glyphs) is replaced by a shipped Lucide set registered under the atrium: icon-set prefix (via the bundle's prependExtension()), rendered with the ux_icon() function and an internal atrium_icon() helper that defaults bare names to the atrium: set. Icon names anywhere in the panel — getNavigationIcon(), Action::icon(), tab/step icons — now also accept any Iconify icon by passing a namespaced name (lucide:rocket, mdi:home, …); bare names resolve to the built-in set and an unknown one degrades to a neutral placeholder instead of erroring. The panel's glyphs change appearance (heroicons → Lucide). New required dependency: symfony/ux-icons. The [@Atrium](https://github.com/Atrium)/icon.html.twig template is removed — apps that overrode it should instead alias or repoint the atrium icon set in config/packages/ux_icons.yaml. Docs: docs/integration-guide/panel/customization.md#icons.

0.1.0

First tagged release: the panel shell, resources, tables, forms, actions, pages, dashboards & widgets, and the data layer described below. Pre-1.0 — the public API may still change in a 0.x minor.

Added

  • Page screens: headings & header actions. Atrium\Page\Page is now a screen descriptor: getTitle()/getHeading()/getSubheading() (computed defaults per screen; the previously-dead ListPage is now used) and getHeaderActions(PageContext): array. Header actions live on the page modelListPage::getHeaderActions() provides the default CreateAction ("New"), and a create/edit page adds its own (override getHeaderActions() on the relevant Page). They are hosted by the screen's Live Component: the DataTable for the list, and the Form component for create/edit, where a server-driven action (e.g. DeleteAction) runs against the loaded record with the confirm flow and a generic post-action redirect (the record is re-resolved; if it's gone — deleted or out of scope — the form redirects to the list). Data/lifecycle hooks remain on the resource (pages aren't DI services). BC: TableConfiguration::headerActions() is removed; the default New button lives on ListPage::getHeaderActions(), and the resource has no header-action method. Docs: docs/integration-guide/pages/overview.md; PRD: docs/PRDs/PRD-page-screens.md. (PAG-01..06.) Public API change.

  • List-screen widgets (header & footer bands). A ListPage can render widget bands above (headerWidgets()) and below (footerWidgets()) its table, composed with the same WidgetSlot + layout primitives as a dashboard via a new Atrium\Page\ListWidgetsConfiguration. Each slot widget receives the resource-identity context (slug, path prefix, labels) as params so it can scope its own query. The widgets are independent of the table's live search/filters (that state lives in the sibling DataTable component) — they reflect the unfiltered resource; live filter-reactivity is intentionally out of scope. The dashboard and list configurations now share an abstract base, Atrium\Widget\WidgetLayoutConfiguration. BC: WidgetSlot moved from Atrium\Dashboard\WidgetSlot to Atrium\Widget\WidgetSlot (it depends only on Widget + the foundational Layout contract). Docs: docs/integration-guide/tables/list-widgets.md; PRD: docs/PRDs/PRD-list-widgets.md. (LW-01..05.) Public API change.

  • Dashboards & widgets. A new widget layer: Atrium\Widget\Widget (an abstract descriptor service, not a Live Component), with two concrete families — StatsWidget (a row of Stat cards) and ChartWidget (a Chart.js chart via Symfony UX Chart.js, no JS build step). One generic Live Component host (Atrium:Widget) mounts any widget by class name (a non-writable, checksummed prop — unforgeable), enforces canView() on mount and refresh, and refreshes each widget independently (manual action or getPollingInterval() polling). Widgets are embeddable anywhere via <twig:Atrium:Widget widget="…" :params="…"> with scalar context. Widgets compute their own data from injected services — the core gains no aggregation API and stays storage-agnostic. Atrium\Dashboard\Dashboard is a routable, navigable, authorizable page; register several. Its dashboard(DashboardConfiguration): DashboardConfiguration (mirroring a resource's table()/form()) arranges widgets — a flat ->widgets([...]) stack, or a ->schema([...]) tree using the same layout components as a form (Grid, Section, Fieldset, Flex) with a WidgetSlot wrapping each widget class. Widget width is a placement concern owned by the slot (it spans like a field), not the widget. Panel routing is generalised — /admin renders the root dashboard (the built-in DefaultDashboard welcome, replaceable at the root slug), and the former /admin/{resource} route is now /admin/{slug}, dispatching to a dashboard or a resource (slugs unique across both; collisions fail fast). The sidebar shows dashboards and resources as two divided groups (the built-in default dashboard appears when the app defines none). New dependency: symfony/ux-chartjs. Docs: docs/integration-guide/widgets/; PRDs: docs/PRDs/PRD-dashboards-widgets.md, docs/PRDs/PRD-dashboard-layout.md. (WGT-03..11, DSH-01..11, PNL-06..08.) New public API.

  • Relation columns. Column::make('author.name') (any depth, e.g. author.company.name) reads through a to-one relation and works everywhere a plain column does — display, sort, search and filter. The Doctrine adapter resolves dotted field names to idempotent LEFT JOINs (rows with a null relation are kept; all values parameter-bound); the in-memory array provider traverses the path via the property accessor. The default label humanises the path (author.name → "Author name"); a null link in the chain renders an empty cell. Docs: docs/integration-guide/tables/columns.md. (TBL-09.)

  • Navigation & access hooks on AdminResource: canAccess() (resource-level gate — hides the nav entry and 403s every page; defaults to canViewAny()), shouldRegisterNavigation() (reachable but hidden from the menu), getNavigationSort() (menu order), and getNavigationBadge() / getNavigationBadgeColor() (a badge next to the entry). The controller filters and sorts the sidebar accordingly and gates create/edit/list on canAccess(). Docs: docs/integration-guide/resources/navigation.md. (Arbitrary render slots remain out of scope.) Public Resource API addition.

  • Action lifecycle hooks on AdminResource: beforeAction/afterAction(string $action, object $record) bracket every row action's handler, and beforeBulkAction/afterBulkAction(string $action, array $records) bracket a bulk action's handler (records pre-filtered to those the user may act on). They run inside the action's transaction. beforeDelete/afterDelete remain the delete-specific convenience. Public Resource API addition.

  • Form validation lifecycle hooks on AdminResource: mutateFormDataBeforeValidate($data, $operation) (clean raw input before validation) and afterValidate($data, $operation) (react to valid data, side-effect only — skipped on an invalid submit). Public Resource API addition.

  • Custom persistence + atomic saves. AdminResource::handleRecordCreation() and handleRecordUpdate(object $record, DataWriterInterface $writer) own the write (defaulting to the data writer), so an app can persist through its own service/command bus without replacing the form. The save path now runs beforeSave → handle* → afterSave inside a single transaction via the new DataWriterInterface::transactional(callable): mixed, so a failing afterSave rolls the write back; deletes (record + bulk) are wrapped the same way. The Doctrine writer uses wrapInTransaction; the array writer just runs the work. Public Resource API + DataWriterInterface addition.

  • Query scoping via AdminResource::scopeQuery(DataQuery): DataQuery (default no-op). Returns a query narrowed to the records the resource exposes (multi-tenancy, ownership, soft-deletes) using DataQuery::withFilters([...]). The scope is applied to the list, the count, select-all bulk actions, and single-record resolution — so an out-of-scope id resolves to null (the edit page 404s; a forged action finds nothing). Unlike the authorization hooks (which hide actions on a still-visible row), scoping removes rows entirely. Docs: docs/integration-guide/data/query-scoping.md. Public Resource API addition.

  • Authorization hooks on AdminResource (canViewAny, canCreate, canEdit, canDelete, canView, dispatched via can()). Enforced server-side in three places: the controller returns 403 for a denied list/create/edit page; the form save re-checks canCreate/canEdit; and the table hides and refuses to run the built-in Edit/Delete/New actions (a bulk delete acts only on permitted records). Custom actions opt in with Action::authorize('ability'); the built-in table actions set theirs automatically. Default is open (security-agnostic core). Public Resource API addition.

  • Record lifecycle hooks on AdminResource: mutateFormDataBeforeFill, mutateFormDataBeforeSave($data, $operation), beforeSave/afterSave($record, $operation), and beforeDelete/afterDelete($record). The save path runs mutate → apply fields → beforeSave → persist → afterSave; delete runs beforeDelete/afterDelete around the built-in delete actions. Public Resource API addition.

  • Integration guide: docs/integration-guide/resources/authorization.md and lifecycle-hooks.md (and a documentation policy + format — see docs/integration-guide/README.md).

Changed

  • mutateFormDataBeforeFill() gained an $operation parameter and now runs on create too. Signature is now mutateFormDataBeforeFill(array $data, string $operation): array; on create it receives the fields' defaults, so it can seed a create form (previously it ran on edit only). Overriders must add the parameter. Public Resource API change (pre-1.0; the hook was added earlier in this same unreleased cycle).
  • DataProviderInterface::find() gained an optional array $filters = [] parameter so record resolution can be scoped (see query scoping above). Callers are unaffected; custom data-provider implementations must add the parameter. Both built-in providers (Doctrine, array) honour it. Public contract change.
  • Table configuration is now a single table(TableConfiguration $table) hook on AdminResource, mirroring form(Schema $schema): Schema. It replaces the separate columns() / recordActions() / headerActions() / bulkActions() methods — set them all on the Atrium\Table\TableConfiguration builder (->columns([…])->recordActions([…])->bulkActions([…])). A fresh TableConfiguration already carries the framework defaults (an Edit record action and a "New" header action), so overriding table() keeps them unless a setter overrides them. Public API change (pre-1.0; the four methods were added earlier in this same unreleased cycle).

Added

  • Table filters (TBL-11): TableConfiguration::filters([...]) renders a filter bar that narrows the query through the data provider. SelectFilter (a categorical dropdown) and TernaryFilter (all / true / false over a boolean field), both extending Atrium\Table\Filter\Filter and declaring their own template for extensibility. Filters resolve to equality conditions carried on DataQuery::$filters and applied by both ArrayDataProvider and DoctrineDataProvider (parameter-bound). Selections live in a writable filterValues LiveProp (with a Reset action); a value for an unconfigured filter name is ignored, so a forged value cannot inject a condition.
  • Column presentation options on Column: ->alignment('right') (alignCenter() / alignRight()), ->width('8rem'), ->boolean() (renders a check/cross icon instead of "Yes"/"No"), ->badge() with ->color('green') or a per-value ->color(fn ($value, $record) => …), and ->visible(false) / ->hidden() to drop a column from the header, cells, search and sort. Cells are now resolved to render-ready descriptors (Column::toCell()); renderValue() is unchanged for direct callers.
  • Table empty state on TableConfiguration: ->emptyState('No articles yet', 'Write your first one…', 'document') renders an icon, heading and optional description when the table has no rows, instead of the bare "No … found." default (which still applies when unconfigured).
  • Table-level default sort and configurable page size on TableConfiguration: ->defaultSort('field', 'desc') orders the first load (until the user sorts; the field need not be a sortable column), and ->paginated(25, [10, 25, 50]) sets the page size and, given options, renders a per-page selector. A client-supplied page size is clamped to a configured choice, so a forged value cannot request an arbitrarily large page.
  • Table header actions and bulk / row-selection actions (TBL-09, TBL-10), built on the existing Atrium\Action subsystem.
    • Configured via TableConfiguration::headerActions() (defaults to a CreateAction) and bulkActions() (defaults to none — returning actions enables row selection). Both are subject-less list<Action>.
    • Built-in Atrium\Table\Action\CreateAction (a primary "New" button linking to the create page) and BulkDeleteAction (a confirmed server action that deletes the whole selection through DataWriterInterface). Header actions now render inside the DataTable Live Component (its card header), not the page chrome, so server-driven header actions work; the hardcoded "New" button was removed from the resource page template.
    • Atrium\Action\Concern\InteractsWithBulkActions — a reusable trait owning a server-driven selection state machine: per-row and whole-page toggles, plus a select-all-matching-the-query mode (a flag with an exclusion list) so a bulk action targets every record across all pages, not just the visible ones. Confirmable requestBulkAction / confirmBulkAction, gated by visibility on both request and run. The selection props are non-writable LiveProps (mutated only through the actions), so a crafted request cannot forge a selection.
    • Atrium\Action\Action gained subject-less rendering (toStandaloneView(), getStandaloneUrl()) and isVisible() for header/bulk bars — which requires a plain bool and fails closed for a subject-bound visibility closure, so a destructive action a developer tried to gate with a closure is never silently exposed. The action button template is parameterised (liveAction) and the confirmation dialog was extracted to a shared components/confirm_modal.html.twig.
    • Known limitation: a select-all bulk action loads every matching record in one request; batched / queued execution for very large selections is deferred.
  • Table record actions + a generic, view-agnostic Atrium\Action subsystem (the shared base for table actions today; header/page/bulk actions next).
    • Atrium\Action\Action — a fluent action (label/icon/color, button()/link()/iconButton() styles, badge(), visible()/hidden(), requiresConfirmation()); it is either a link (url()) or a server action (action(Closure)). Atrium\Action\ActionGroup renders a set as a no-JS <details> dropdown. Both implement ActionContract so a host renders and runs them polymorphically, and each declares its own getTemplate() so custom actions/groups can ship their own renderer.
    • Built-in table actions Atrium\Table\Action\EditAction (link to the edit page) and DeleteAction (a confirmed server action that deletes through DataWriterInterface). AdminResource::recordActions() declares them (defaults to Edit); a resource picks any mix of links, server actions and groups.
    • DataTable renders the actions column and runs server actions via the reusable Atrium\Action\Concern\InteractsWithActions trait: a server-driven confirmation (no client JavaScript) gated by visibility on both request and run, so a crafted request can neither surface nor execute a hidden action. Rendering lives in reusable components/action{,s,_group}.html.twig partials.
  • Phase 0 boilerplate: installable Symfony 8.1 bundle skeleton.
    • AtriumBundle (AbstractBundle) with path_prefix / brand configuration and atrium.resource autoconfiguration.
    • Core contracts and value objects: AdminResource, ResourceRegistry, Column, DataProviderInterface, DoctrineDataProvider (stub).
    • Engineering harness: PHPUnit, PHPStan (max), PHP-CS-Fixer (Symfony ruleset), GitHub Actions CI, and unit tests for Column and ResourceRegistry.
  • Phase 1 engineering harness — data-layer and functional coverage.
    • DoctrineDataProviderTest exercising count() and fetch() offset/limit pagination against a real in-memory SQLite EntityManager (EntityManagerFactory + Product fixture entity).
    • KernelBootTest + AtriumTestKernel (MicroKernel): boots FrameworkBundle + TwigBundle + AtriumBundle and asserts bundle registration, ResourceRegistry wiring, autoconfiguration-based resource discovery (RES-02), exposed configuration parameters, and [@Atrium](https://github.com/Atrium) Twig rendering.
    • Added symfony/var-exporter (dev) and enabled Doctrine native lazy objects in the test EntityManager (PHP 8.4+ requirement under ORM 3.6 / var-exporter 8).
    • Open-source hygiene: CODE_OF_CONDUCT.md (Contributor Covenant 2.1), GitHub issue templates (bug report, feature request) and a pull request template.
  • Reactive panel vertical slice (TBL-01..08, PNL-01..04, DAT-01..02).
    • Backend-agnostic read layer: DataQuery value object; DataProviderInterface now takes a query; DoctrineDataProvider builds a parameter-bound QueryBuilder (search/sort/paginate/count); new in-memory ArrayDataProvider.
    • Column value extraction + formatting (scalars, dates, enums, bool, arrays, null) with a custom formatStateUsing() callback.
    • DataTable Live Component (search bound to the URL, click-to-sort, pagination, empty/loading states) and its Twig template.
    • Panel shell: AdminController with parametric routes (/admin, /admin/{resource}), Tailwind layout with registry-driven sidebar nav, dashboard and resource pages. Doctrine wiring activates only when DoctrineBundle is present.
    • Self-contained styling: the bundle ships a precompiled Tailwind stylesheet (assets/dist/atrium.css, built from its own templates via the standalone CLI — no Node) exposed through an AssetMapper path and an atrium_stylesheet() Twig helper, so consumers need no Tailwind configuration. The JS entrypoint is rendered via atrium_importmap().
    • Functional tests: panel routes (WebTestCase) and reactive component behaviour (search/sort/paginate via InteractsWithLiveComponents).
    • Promoted symfony/ux-live-component, symfony/ux-twig-component, symfony/routing and symfony/http-foundation to runtime requirements.
  • Forms, persistence and Pages (FRM-01..06, DAT-03, LAY-03..05).
    • Schema + fluent Field hierarchy: Text (email/url), Textarea, Number, Select (static + optionsUsing() for dependent selects), Checkbox, Date, DateTime — with normalize/format and Symfony-constraint validation.
    • DataWriterInterface (DAT-03) with DoctrineDataWriter + in-memory ArrayDataWriter; DataProviderInterface::find() for single-record loads.
    • Form Live Component: hydrate/dehydrate via formData LiveProp, inline validation (keeps last values), reactive ->live() fields driving dependent selects, save through the writer, success notice / redirect. Field rendering is split into per-type widget partials under components/form/widget/; each Field declares its widget template via getTemplate() (and label placement via rendersOwnLabel()), so third-party apps can add custom field types that ship their own templates from any bundle — no change to the renderer.
    • Page descriptor classes (Page, ListPage, CreatePage, EditPage) with a getRedirectUrl() hook and PageContext; AdminResource::pages(). Generic dispatcher routes — /admin/{resource}/new and /admin/{resource}/{id}/edit — resolve resource + action → Page → embed the Form component, so adding a CRUD resource needs no route registration.
    • Added symfony/validator as a runtime requirement.
  • docs/integration-guide/forms/custom-fields.md — documents the custom field type extension point (the getType() / getTemplate() / rendersOwnLabel() contract, the widget template context, and a worked CountrySelect example).
  • Forms v2 / M1 — schema tree & layout (SCH-01..08). A form Schema is now a tree of Atrium\Layout\Components (fields + layout containers), not a flat list.
    • New top-level Atrium\Layout subsystem (view-agnostic, reusable by future dashboards/infolists): Component contract, LayoutComponent base, Grid/Flex/Section/Fieldset, and placement concerns HasColumnSpan (columnSpan()/columnSpanFull()) and HasGrow (grow() for Flex children; Flex::from() sets the row's breakpoint).
    • Schema::components([...]) builds the tree; Schema::fields([...]) is kept as the flat shortcut. Schema::getFields() flattens leaves depth-first for hydration/validation; getComponents() exposes the tree for rendering.
    • A generic recursive renderer (components/layout/*.html.twig) lays nodes out in responsive grids; grid utilities are safelisted in assets/atrium.css ([@source](https://github.com/source) inline(...)) since they're composed in PHP.
    • Forms v2 PRD addendum updated to mark M1 delivered.
  • Forms v2 / M2 — conditional field visibility (FRM-08, FRM-09) + the Get state accessor (FRM-11).
    • Atrium\Form\Get — an invokable read accessor over the form state ($get('field')), handed to field callbacks.
    • Atrium\Form\Concern\HasVisibility on Field: visible(bool|Closure), hidden(bool|Closure), visibleOn(op), hiddenOn(op) — all server-evaluated during the Live Component re-render (no client logic).
    • The Form component filters hidden fields out of the render tree (cloning containers, dropping any left empty) and skips them in validation/hydration; it exposes operation() (create/edit).
  • Forms v2 / M3 — cross-field reactivity, the Set accessor, validation DX and presentation niceties (FRM-10..13).
    • Atrium\Form\Set — invokable write accessor over form state ($set('f', v)).
    • Atrium\Form\Concern\HasReactivity on Field: afterStateUpdated(Closure) (auto-implies live()); the callback gets ($state, Get, Set) and can derive one field from another (e.g. SKU from name). The Form component diffs formData against the previous render in a #[PreReRender] pass to detect the changed field — robust to per-field sub-path model writes.
    • Atrium\Form\Concern\HasValidationRules on Field: maxLength(), minLength(), length(), regex() (→ Length/Regex); NumberField gains min()/max() (→ GreaterThanOrEqual/LessThanOrEqual). rules([...]) stays the escape hatch.
    • Presentation: placeholder() (Text/Textarea/Number via HasPlaceholder), autofocus(), hiddenLabel() on Field; widgets + wrapper updated.
  • Forms v2 / M5 — Wizard (SCH-10). Atrium\Layout\Wizard + Atrium\Layout\Step, a multi-step container, plus a WizardForm Live Component.
    • WizardForm extends Form: the wizard interaction (Next/Back, gated advancement, a header that jumps backwards) is layered on the unchanged hydrate/validate/save core. Form was made extensible for this — non-final, with protected collectErrors()/fieldsIn()/schema() and a focusContainer() hook the subclass overrides to focus an errored step.
    • The current step is server state (currentSteps prop); Next validates only the current step's fields before advancing, Back is free, and the wizard owns submit (Submit appears on the last step). All panels render (inactive hidden), so navigating keeps in-progress input.
    • A resource selects its form component via the new AdminResource::getFormComponentName()Atrium:Form by default, upgrading to Atrium:WizardForm when the schema contains a Wizard. The form page embeds it dynamically ({{ component(resource.formComponentName, …) }}).
    • Step::make('Label')->icon()->description()->columns()->schema([…]).
  • Forms v2 / M5 — Tabs (SCH-10). Atrium\Layout\Tabs + Atrium\Layout\Tab, a tabbed container on the same schema tree.
    • The active tab is server state on the host Live Component (a selectTab live action + an activeTabs prop), so it survives unrelated re-renders and needs no client JavaScript. Every panel is rendered each request with inactive ones hidden, so switching only flips an attribute — inputs in other tabs stay in the DOM and keep their in-progress values across the morph. A failed save focuses the first tab holding a validation error.
    • Tab::make('Label')->icon(…)->badge(…)->columns(…)->schema([…]); Tabs has a stable id (auto-derived from its tabs, or ->id()) used to key the state.
  • Forms v2 / M5 (cont.) — cross-field validation, inline labels and the add/remove row editors that complete Tags / Key-value (FRM-12, FRM-13, FLD-06, FLD-07).
    • ->same(field) / ->different(field) (FRM-12) — cross-field comparison rules the Form evaluates against the full submitted state (a Symfony constraint can't see a sibling field); each takes an optional custom message.
    • ->inlineLabel() (FRM-13) — render a field's label beside the input in a responsive column instead of above it.
    • Tags / Key-value are now full row editors (FLD-06/FLD-07): one input per tag (a key + value input per pair), with add/remove handled by generic addRow/removeRow Live Component actions. Fields opt in via the new Atrium\Form\Field\RepeatableField contract, so the renderer stays generic; empty rows are dropped on save by the existing normalize().
  • Forms v2 / M5 (partial) — container visibility, dehydrated(false), and the Tags / Key-value fields.
    • Container-level visibility: visible()/hidden()/visibleOn()/hiddenOn() now apply to layout containers too (a hidden Section/Grid/Flex/Fieldset drops its whole subtree). Visibility moved to Atrium\Layout\Concern\HasVisibility over a generic Atrium\Layout\StateAccessor (implemented by Atrium\Form\Get), keeping Atrium\Layout free of any form dependency.
    • Field::dehydrated(false) (FRM-14) — a field shown and validated but not written to the model on save.
    • TagsField (FLD-06, list<string>) and KeyValueField (FLD-07, array<string,string>) — zero-JS, server-driven (comma string / key: value lines) with chip / preview rendering.
  • Forms v2 / M4 — new field types (FLD-01..05): RadioField, ToggleButtonsField (both extend SelectField), ToggleField (a switch, extends CheckboxField), ColorField, and HiddenField. Field::rendersInLayout() (default true; false for HiddenField) keeps a hidden value in the form state — validated and persisted — while drawing no widget and taking no grid cell. Widgets under components/form/widget/{radio,toggle,color,toggle_buttons}.html.twig.
  • Content components (CNT-01..03) — static building blocks for a schema (Atrium\Content\Text, UnorderedList, Image). They implement the shared Atrium\Layout\Component contract but carry no form state: never hydrated or validated, and skipped by Schema::getFields(). Text supports semantic colour, size, weight, badge and raw-HTML modes. Useful for headings, instructions and callouts placed beside fields (with columnSpan/grow).

Changed

  • UI refresh — a modern, polished look across the panel:
    • New shell: a sticky, backdrop-blurred sidebar with a gradient brand mark, a grouped icon navigation (inline-SVG [@Atrium](https://github.com/Atrium)/icon.html.twig set) and a filled active state; a sticky, blurred topbar.
    • Dashboard with icon-tiled resource cards (hover lift) and a glowing welcome card; resource list pages gained a "New " action button.
    • Form sections are now header + divider + padded cards; the form page is a centered column (no double-card), with a spinner-backed save state and a check icon on the success notice.
    • Data table: search with an inline icon, uppercase column headers, a bold first column, row hover, and icon pagination controls.
    • Inputs rounded to lg with colour transitions. All accents use primary.
  • Theming: the panel accent is now a single semantic primary colour (default palette "blue-energy"). Templates only use *-primary-* classes, so re-skinning is one change — edit the eleven --color-primary-* values in assets/atrium.css and composer build-css, or override --color-primary-* at runtime in a stylesheet loaded after atrium.css (no rebuild; the utilities resolve the CSS variables). Replaces the previous hard-coded indigo accent.
  • Built-in concrete Field types (TextField, TextareaField, NumberField, CheckboxField, SelectField, DateField, DateTimeField) are now non-final so applications can subclass them to add custom fields; the abstract Field remains the public contract.
  • Form widgets now carry full dark-mode variants — labels, inputs, help/error text, the success notice and the checkbox — so forms are legible in dark mode.
  • The form page card is now full-width (removed the max-w-2xl constraint).
  • BC (pre-release): Field::getTemplate() now returns the field's layout wrapper; the input widget moved to the new Field::getWidgetTemplate(). Custom field types that shipped their own widget should override getWidgetTemplate() instead of getTemplate() (see docs/integration-guide/forms/custom-fields.md).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor
spatie/laravel-javascript-views