Skip to content

chore: upgrade to Lattice 0.72 and overhaul the kit - #7

Merged
bambamboole merged 6 commits into
mainfrom
chore/lattice-0.72
Sep 5, 2026
Merged

bambamboole merged 6 commits into
mainfrom
chore/lattice-0.72

Conversation

@bambamboole

@bambamboole bambamboole commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Brings the starter kit from Lattice 0.34 (main) to 0.72.2 and gives the surrounding tooling — and the teams surface — the same pass. No new product features: the kit still has teams, Fortify auth, 2FA and passkeys, and nothing more.

Lattice 0.72

  • Layout chrome moved to Lattice\Ui\Components (0.62), StackDirection folded into Orientation and Text::align() took its own TextAlign (0.70), and handle() now receives validated FormData instead of a Request (0.60).
  • Records reach a definition through context resolvers registered in AppServiceProvider, not through hand-threaded ['team' => …] context or route parameters. member, invitation and passkey resolve inside their parent, so a forged id in a sealed reference 404s instead of reaching another team's records.
  • Authorization is declared on the definition attributes (can: 'removeMember', on: 'team'), replacing eight authorize() bodies. Page middleware drops the re-declared 'web' that 0.43 started merging in.
  • Browser tests target data-test with the full node identity (0.62 + 0.72).

Bugs this surfaced

  • visibleFrom() / hiddenFrom() did nothing. @lattice-php/core owns the responsive-visibility wrapper (hidden md:contents), and nothing pointed Tailwind at that package's dist, so md:contents was never generated and every such node stayed hidden at all widths. The same missing glob is why the members and invitations tables rendered without a header row.
  • The sidebar had no trigger. It was collapsible(), but Lattice ships no trigger of its own — below md the drawer never opened, so a phone had no team switcher, no navigation and no logout. A topbar now carries the toggle, the breadcrumbs and the user menu.
  • DashboardPage::breadcrumbs() was dead code — no layout ever rendered a Breadcrumbs node. Callouts joins it so a flashed effect has somewhere to land.

Teams performance

before after
TeamsTable source, 6 teams 8 queries 1
members-table row actions, 6 members 13 queries 2

User::teams() now uses the Membership pivot class, so the cast role is read off the loaded row instead of re-queried. AuthorizesRowActions caches a row action's policy check on the definition instance — which lives exactly one render, unlike a cache on the model, where a later attach() goes stale (the suite caught that attempt).

Overhaul

  • Dropped: Wayfinder (no file imported a generated helper), Chisel and Sail (never referenced), components.json (shadcn leftover), concurrently (Laravel 13 ships artisan dev), the bunny font fetch (self-hosted @fontsource-variable instead), and the 12 KB commented tsconfig.
  • Shared shell: AppPage carries the layout, middleware and notification listeners for the four signed-in pages; PageHeader replaces ten hand-built heading-plus-lead stacks.
  • CI: one lint job + one all-in-one test job → four workflows behind a shared composite setup action, so a flaking browser run gets its own status check and only the jobs needing node pay for it. The js job fails on a stale lattice/generated.d.ts.
  • Local gate: .githooks pre-commit (format staged files) and pre-push (PHPStan, suite, build — scoped to the push), wired by composer install.
  • Static analysis: PHPStan 5 → 8 over app/ and tests/, no baseline. Rector joins the gate with the Laravel 13 / PHP 8.4 sets.
  • Agent rules: .ai/guidelines + .ai/rules compiled into the git-ignored CLAUDE.md/AGENTS.md by Boost.

Verification

composer ci:check green (68 feature/unit tests, PHPStan 0, Rector clean, oxlint/oxfmt/tsc clean), composer test:browser green (5 tests, two of them new: the mobile drawer and the breadcrumb trail), npm run build green.

Lattice 0.59 to 0.72 on both sides, Laravel 13.30, Pest 5.1, TypeScript 7,
Vite 8. Adds @lattice-php/form as a direct dependency: 0.62 moved the form
controls out of the ui package, and the passkey components import them.

Drops what the kit no longer uses: Wayfinder generated no route helper any
file imported, Chisel and Sail were never referenced at all, and
components.json outlived the shadcn token layer that was removed in 0.34.
concurrently goes with them — Laravel 13 ships `artisan dev`. The bunny font
fetch becomes a self-hosted @fontsource-variable package, so the blade
template no longer needs @fonts or a per-page vite entry that has no
resources/js/pages to resolve against.

resources/js/lattice/generated.d.ts is now committed and checked in CI; it is
what declares the wire props of the two custom components.
0.62 moved the layout chrome into Lattice\Ui\Components and the form controls
into @lattice-php/form; 0.70 folded StackDirection into Orientation and gave
Text its own TextAlign enum; 0.60 validates a form once and hands handle() the
validated FormData instead of a Request.

The user menu's hand-rolled avatar SVG becomes the Avatar component, which
Lattice has shipped since 0.51.
AppServiceProvider registers the team, member, invitation and passkey context
keys. A registered key cascades into every child component, so a page that
types `Team $team` in render() already seeds it — the explicit
`['team' => $team->slug]` at each Form::use() and Table::lazy() call goes away,
and so does the ResolvesTeamFromContext trait.

The dependent keys resolve inside their parent rather than by a bare
findOrFail() on the id, so a forged member, invitation or passkey id in a
sealed reference now 404s instead of reaching another team's records. The
passkey test asserts that 404 in place of the old 403.

Authorization moves onto the definition attributes as `can`/`on`, which
replaces eight authorize() bodies. A denied definition is hidden at render
time, so the tests that assert a member cannot update or delete a team switch
to the submitDeniedForm() helper.

Also fixes the nullability the raised PHPStan level surfaced: Team::owner()
returns ?User rather than ?Model, an invitation's team is not nullable (the
foreign key is NOT NULL and cascades), and a pivot present on a members()
result is not either.
One lint job plus one all-in-one test job becomes four workflows behind a
shared composite setup action, so a slow browser run no longer holds up the
status check that carries the fast suites, and only the jobs that need node or
a built bundle pay for them. The js job also fails on a stale
lattice/generated.d.ts.

Adds the .githooks pair composer install already points core.hooksPath at:
pre-commit auto-fixes staged files, pre-push runs PHPStan, the suite and the
build, scoped to what the push touches.

PHPStan moves from level 5 over app/ to level 8 over app/ and tests/, with no
baseline. Rector joins the gate with the Laravel 13 and PHP 8.4 sets; its
first pass produced the #[RouteKey] and #[Table] attributes on the models.
Dependabot now watches composer and npm as well as the actions.
.ai/guidelines carries how to run and verify the kit; .ai/rules carries the
path-scoped conventions an agent has to know before editing — how a definition
reads context and declares authorization, why the dependent keys resolve inside
their parent, that there are no page components to write, and which data-test
identity a browser test targets. Laravel Boost compiles both into the
git-ignored CLAUDE.md / AGENTS.md.

boost.json drops the wayfinder skill and picks up testing-best-practices.
A feature test renders a real route, so `@vite` threw without a built manifest
— which is why the whole suite quietly depended on `npm run build` having run.
Tests\TestCase now calls withoutVite(); the new Tests\BrowserTestCase turns
that back off for the one suite that really does drive the built bundle. The
php-tests CI job needs no node toolchain as a result.
@bambamboole
bambamboole merged commit f25bf9d into main Sep 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant