Skip to content

fix: repair the app shell, and stop querying per table row - #8

Merged
bambamboole merged 5 commits into
mainfrom
fix/team-shell-and-queries
Sep 5, 2026
Merged

bambamboole merged 5 commits into
mainfrom
fix/team-shell-and-queries

Conversation

@bambamboole

Copy link
Copy Markdown
Contributor

Follow-up to #7, taking the same pass over the teams surface. Three real bugs, one performance fix, and the deduplication that fell out of them. No new features.

Bugs

visibleFrom() / hiddenFrom() did nothing. @lattice-php/core owns the responsive-visibility wrapper (hidden md:contents), and nothing pointed Tailwind at that package's dist — @lattice-php/ui/css sources only its own, and the Vite plugin sources only component packages (the ones declaring extra.lattice.plugin). So md:contents was never generated and every such node stayed hidden at all widths, silently. The same missing glob is why the members and invitations tables rendered without a header row.

This affects any app installing Lattice from npm; the fix here is one @source line, but the plugin arguably ought to source the runtime packages itself.

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 way to log out. A topbar now carries the toggle, the breadcrumb trail and the user menu.

DashboardPage::breadcrumbs() was dead code — no layout ever rendered a Breadcrumbs node. Callouts joins it so an effect flashed from a controller or listener has somewhere to land.

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 already-loaded row instead of re-queried per team. AuthorizesRowActions caches a row action's policy check on the definition instance, which lives exactly one render. Caching on the User model instead looked tempting and is wrong — a later attach() leaves the loaded relation stale, which is what the suite caught.

Deduplication

AppPage carries the layout, middleware and notification listeners for the four signed-in pages, leaving each its own route, name and gate; SettingsPage overrides the middleware to drop verified, since its profile tab is where an unverified user resends the mail. PageHeader replaces ten hand-built heading-plus-lead stacks.

Verification

composer ci:check green, composer test:browser green — including two new browser tests: the mobile drawer opening, and the breadcrumb trail after navigating into a team.

Both traps are recorded in .ai/rules/.

@lattice-php/ui/css sources its own dist and the Vite plugin sources every
discovered component package, but neither covers the sibling runtime packages.
@lattice-php/core owns the responsive-visibility wrapper (`hidden md:contents`),
so `md:contents` was never generated and every visibleFrom()/hiddenFrom() node
stayed hidden at all widths — silently, since the class simply did not exist.

The same glob restores the table header and cell utilities that only appear in
the table package's dist, which is why the members and invitations tables
rendered without a header row.
The sidebar was collapsible but nothing could toggle it, and Lattice ships no
trigger of its own — below the md breakpoint the drawer stayed shut, so a phone
had no team switcher, no navigation and no way to log out. A topbar now carries
the toggle, the breadcrumb trail, and the user menu.

DashboardPage has declared breadcrumbs() since it was written, but no layout
ever rendered a Breadcrumbs node, so the trail was dead code. Callouts joins it
so an effect flashed from a controller or listener has somewhere to land.
TeamsTable asked teamRole() for a role the membership pivot had already
loaded — 8 queries for 6 teams. User::teams() now uses the Membership pivot
class, so the cast role is read straight off the row: 1 query.

TeamMembersTable and TeamInvitationsTable ran their policy checks inside
actions(), which a table calls once per row: 13 queries for 6 members. The new
AuthorizesRowActions concern caches the answer on the definition instance,
which lives exactly one render — 2 queries, one per ability. Caching on the
User model instead looked tempting and is wrong: a later attach() leaves it
stale, which is exactly what the suite caught.
Four pages repeated the same layout and middleware; AppPage carries them and
the notification listeners, leaving each page its own route, name and gate.
SettingsPage overrides the middleware to drop `verified` — its profile tab is
where an unverified user resends the mail.

PageHeader replaces ten hand-built heading-plus-lead stacks, and TeamPage and
TeamsPage gain the breadcrumbs the new trail can now show.
@bambamboole
bambamboole merged commit 159564e 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