Master-Detail
The master-detail pattern shows an entity list alongside a detail pane. Selecting an item in the list reveals its details without leaving the page. This page covers configuration, the selection model, route-driven detail panes, panel modes, and keyboard navigation.
Overview
The master-detail pattern is the default experience on tablet and desktop when EntitySelectionMode.responsive is configured. On phones, selecting a row navigates to a full-page detail route instead.
Configuration
Enable master-detail through EntityRouting<T>:
final courseConfig = EntityConfiguration<Course>(
metadata: EntityMetadata(
identifier: 'courses',
name: 'Course',
pluralName: 'Courses',
icon: FluentIcons.book_24_regular,
),
api: const CourseApi(),
routing: EntityRouting<Course>(
path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
builder: StandardRouteBuilder<Course>(),
mode: EntitySelectionMode.responsive,
),
layouts: EntityLayouts<Course>(
list: [courseTableLayout],
details: [
courseDetailLayout,
courseParticipantsLayout,
EntityVersionLayout<Course>(),
EntityAuditLayout<Course>(),
],
),
actions: EntityActions<Course>(
inline: StandardEntityActions.inline<Course>(),
header: StandardEntityActions.header<Course>(),
),
);The entries in EntityLayouts.details become the tabs in the detail pane. Each one is an EntityLayout<T> with its own icon, title, optional Authorize gate, and build method.
Workspace Shells
Two shells host the master-detail layout:
StandardRouteBuilder<T>— the standard shell. Renders the list and detail in a singleEntityShellLayout.EntityWorkspaceRouteBuilder<T>— the newer workspace shell. Mounts the list inEntityWorkspace<T>with a generatedEntityWorkspaceConfig, enabling workspace-wide chrome, side panes, and slot-based composition.
Use the workspace builder when you want richer chrome (saved-view state, draft banners, workspace lifecycle hooks, embedded sub-resources, etc.); use StandardRouteBuilder for the compact standard shell.
Selection Model
Selection lives on the EntityListController<T> that backs the list. Read it through EntityProvider:
final controller = EntityProvider.listControllerOf<Course>(context);
// Single-selection (default)
final current = controller.selectedEntity; // Course?
// Multi-selection (when enableMultiSelect: true)
final isMulti = controller.multiSelectMode;
final selected = controller.selectedEntities; // Set<Course>Single Selection
The default. Clicking a row sets selectedEntity. The detail pane updates without a navigation event. Clicking the same row again deselects it (and closes the pane).
Multi-Selection
When the layout sets enableMultiSelect: true, checkboxes appear and selectedEntities accumulates. The detail pane is replaced by the batch action bar (see Batch Operations).
Route-Driven Detail Panes
Detail tabs are routed, not stateful. Current navigation uses NavSlice: tab identifies the top-level partition (approved, drafts), while section identifies the detail subsection. Each detail tab adds ?section=<identifier> to the URL so the user can deep-link, share, or refresh without losing their place:
// Build a URL that opens a course at the Participants tab
final route = vyuh.entity?.getConfig<Course>()?.route;
final url = route!.view(
courseId,
const NavSlice(section: 'participants'),
);
context.go(url);The route builder reads the section query parameter and selects the corresponding EntityLayout<T> from EntityLayouts.details. Tabs gated by Authorize are filtered out of the bar before the route resolves.
Panel Modes
Docked (desktop, > 1200px)
Both surfaces are visible at once. The pane resizes with the window; the table column visibility adapts to the remaining space.
Overlay (tablet, 600 - 1200px)
The detail pane floats over the list. Tapping outside the pane closes it. The list stays visible but dimmed.
Full-Page (phone, < 600px, or EntitySelectionMode.navigate)
Selecting a row navigates to the detail route at /lms/courses/:id. The back button returns to the list.
Responsive Transitions
The shell transitions panel modes automatically when the window crosses a breakpoint:
The selected entity is preserved across transitions when possible.
Detail Tabs
final courseDetailLayout = EntityDetailLayout<Course>(
identifier: 'details',
title: 'Details',
icon: FluentIcons.info_24_regular,
build: (context, course) => CourseDetailView(course: course),
);
final courseParticipantsLayout = EntityDetailLayout<Course>(
identifier: 'participants',
title: 'Participants',
icon: FluentIcons.people_24_regular,
authorize: Authorize.permission('lms.enrollments.view'),
build: (context, course) => CourseParticipantsView(course: course),
);Tabs that fail their authorize check are hidden from the tab strip entirely.
Keyboard Navigation
The shell registers shortcuts on desktop:
| Keys | Action |
|---|---|
Cmd/Ctrl + K | Open the command palette |
J / K | Move selection down / up in the list |
Enter | Open the focused row's detail (or first tab) |
Esc | Clear the selection / close the detail pane |
1 … 9 | Switch detail tabs |
Override or extend these by registering shortcuts at the Shortcuts / Actions widgets that wrap EntityShellLayout / EntityWorkspace.
Next Steps
- Responsive Layouts — breakpoint-driven layout decisions
- Batch Operations — the multi-select / bulk action flow
- Building UI —
EntityListView,EntityProvider, and selection callbacks