Skip to content

Responsive Layouts

The Vyuh Entity System adapts entity views across mobile, tablet, and desktop form factors. This page covers EntitySelectionMode, breakpoint- driven layout decisions, column priority, and the workspace shell.

EntitySelectionMode

EntitySelectionMode controls how a row tap in the list is handled:

dart
enum EntitySelectionMode {
  /// No navigation — only fires the controller's selection callback.
  /// Use when the list is a picker or selection-only surface.
  none,

  /// Navigate to the detail route. The default.
  navigate,

  /// Adapt by viewport: navigate to a route on phone, show a side panel
  /// (overlay or docked) on tablet/desktop.
  responsive,
}

Set it on the EntityRouting configuration:

dart
EntityRouting<Course>(
  path: NavigationPathBuilder.collection(prefix: '/lms/courses'),
  builder: StandardRouteBuilder<Course>(),
  mode: EntitySelectionMode.responsive,
)
ModePhone (< 600)Tablet (600 - 1200)Desktop (> 1200)
noneCallback onlyCallback onlyCallback only
navigateDetail routeDetail routeDetail route
responsiveDetail routeOverlay panelDocked panel

Breakpoints

The entity system uses three breakpoint tiers:

BreakpointWidthTypical device
Small< 600Phone
Medium600 – 1200Tablet, narrow desktop
Large> 1200Standard desktop

The Workspace Shell

EntityWorkspace<T> (mounted by EntityWorkspaceRouteBuilder<T>) is the container that drives the responsive experience. It provides:

  • The navigation sidebar (collapsible by category)
  • The command palette (Cmd/Ctrl+K)
  • The list pane and the detail pane (docked or overlay)
  • Per-form-factor breakpoint handling

EntityShellLayout (mounted by StandardRouteBuilder<T>) remains the standard route-builder container. The workspace route builders wrap the same list/detail behavior with richer workspace chrome and lifecycle hooks, so new entity surfaces usually start there.

Panel Modes

ModeWhen
DockedDesktop default — pane is fixed alongside the list
OverlayTablet default — pane floats above the list
Full-pagePhone default — selection navigates to a detail route

The mode is chosen automatically from the breakpoint and the EntitySelectionMode.

Mobile Behavior

On phones (< 600px):

  • EntityTableConfig<T> falls back to a compact mobile list. Each row shows the entity's name, subtitle, key properties, and any status indicator.
  • Tapping an entity opens a bottom sheet with the detail layout (when mode: responsive) or navigates to the detail route (when mode: navigate).
  • EntityGridConfig<T> collapses to a single column.
dart
final courseGridLayout = EntityGridConfig<Course>(
  identifier: 'grid',
  title: 'Cards',
  icon: FluentIcons.grid_24_regular,
  defaultColumns: 3,
  minColumns: 1,
  maxColumns: 5,
  userAdjustableColumns: true,
  card: EntityCardLayout<Course>(
    title: (c) => c.name,
    subtitle: (c) => c.level,
    leading: (context, c) => const Icon(FluentIcons.book_24_regular),
    properties: [
      (FluentIcons.tag_24_regular, 'Level', (c) => c.level),
      (FluentIcons.checkmark_circle_24_regular, 'Status', (c) => c.status),
    ],
  ),
);

Tablet Behavior

On medium screens (600 - 1200px):

  • The table renders fewer columns. Columns with priority: ColumnPriority.low hide first, then normal.
  • The detail pane appears as an overlay above the list.
  • The sidebar collapses to icons by default.

Desktop Behavior

On large screens (> 1200px):

  • All columns are visible.
  • The detail pane docks alongside the list.
  • A layout switcher appears when the entity has more than one CollectionLayout<T> registered (e.g. table + grid).
dart
EntityLayouts<Course>(
  list: [courseTableLayout, courseGridLayout], // Switcher available
  details: [courseDetailLayout],
)

Column Priority

ColumnPriority constants control which fields hide first as space tightens:

ConstantValueBehavior
ColumnPriority.essential0Never hidden
ColumnPriority.high1Hidden only on very narrow screens
ColumnPriority.normal2Hidden on medium screens (default)
ColumnPriority.low3Hidden first

Annotation form:

dart
@Field(label: 'Name', priority: ColumnPriority.essential)
final String name;

@Field(label: 'Duration (min)', priority: ColumnPriority.low)
final int durationMinutes;

Hand-written form:

dart
static final name = TextFieldDef<Course>(
  field: 'name',
  label: 'Name',
  priority: ColumnPriority.essential,
);

Layout Selection per Form Factor

The framework chooses a single CollectionLayout<T> to render. When you register more than one, the first one in the list is the default and the user can switch between them via the layout switcher.

A common pattern:

dart
EntityLayouts<Course>(
  list: [
    courseTableLayout,   // Default — best for desktop
    courseGridLayout,    // Visual scan — great for tablet
  ],
  details: [
    courseDetailLayout,
    courseParticipantsLayout,
  ],
)

To choose programmatically based on a layout-builder (LayoutBuilder / MediaQuery), wrap your custom layout's build in a LayoutBuilder:

dart
class CourseHybridLayout extends CollectionLayout<Course> {
  @override
  Widget build(BuildContext context, EntityListController<Course> controller) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
          return CourseMobileList(controller: controller);
        }
        return CourseTable(controller: controller);
      },
    );
  }
}

Workspace Shape Selection

EntityWorkspaceConfig<T> lets you pick the shape of the workspace:

  • Master-detail (default) — list pane + detail pane.
  • Singleton — one entity, no list (for Settings-style entities marked isSingleton: true).
  • Grouped — a master pane plus an embedded detail with multiple sub-resources (used by Inbox).
  • Dashboard — a single dashboard layout with no list at all.

The generator emits a $courseWorkspaceConfig() factory that picks the right shape from the @Entity declaration. Override routing.builder if you need a different shape:

dart
routing: EntityRouting<Settings>(
  path: NavigationPathBuilder.singleton(prefix: '/config/settings'),
  builder: EntityWorkspaceRouteBuilder<Settings>(
    workspaceConfig: $settingsWorkspaceConfig(),
  ),
),

LMS Example: Browsing Courses Across Form Factors

dart
class CourseFields extends EntityFieldRegistry<Course> {
  static final name = TextFieldDef<Course>(
    field: 'name',
    label: 'Name',
    primary: true,
    priority: ColumnPriority.essential,
  );
  static final level = EnumFieldDef<Course>(
    field: 'level',
    label: 'Level',
    priority: ColumnPriority.high,
    options: const {
      'beginner': 'Beginner',
      'intermediate': 'Intermediate',
      'advanced': 'Advanced',
    },
  );
  static final status = EnumFieldDef<Course>(
    field: 'status',
    label: 'Status',
    priority: ColumnPriority.high,
    options: const {
      'draft': 'Draft',
      'published': 'Published',
      'archived': 'Archived',
    },
  );
  static final duration = NumberFieldDef<Course>(
    field: 'duration_minutes',
    label: 'Duration',
    suffix: 'min',
    priority: ColumnPriority.low,        // Desktop only
  );
  static final instructor = ReferenceFieldDef<Course>.auto(
    foreignKeyField: 'instructor_id',
    targetField: 'name',
    targetEntityType: 'trainers',
    label: 'Instructor',
    priority: ColumnPriority.normal,     // Hidden on tablet
  );

  @override
  List<FieldDefinition<Course>> get all =>
      [name, level, status, duration, instructor];
}

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, courseGridLayout],
    details: [courseDetailLayout, courseParticipantsLayout],
  ),
  fields: CourseFields().all,
  actions: EntityActions<Course>(
    inline: StandardEntityActions.inline<Course>(),
    header: StandardEntityActions.header<Course>(),
  ),
);

Result:

  • Phone — compact list, bottom-sheet detail with tabs.
  • Tablet — table with Name, Level, and Status; overlay detail pane.
  • Desktop — full table with all columns; docked detail pane; layout switcher.

Next Steps