> ## Documentation Index
> Fetch the complete documentation index at: https://docs.presentum.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Outlets

> Learn how outlets render presentations in your UI

## What are outlets?

Outlets are widgets that render presentations. They watch a specific surface and rebuild when the active item changes.

Outlets contain **zero business logic**. They just render what the engine tells them to show.

<Tip>
  Think of outlets as "slots" in your UI where presentations appear. The engine
  controls what goes in the slot.
</Tip>

## Basic outlet

The simplest outlet uses `PresentumOutlet`:

```dart theme={null}
class HomeTopBannerOutlet extends StatelessWidget {
  const HomeTopBannerOutlet({super.key});

  @override
  Widget build(BuildContext context) {
    return PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
      surface: AppSurface.homeTopBanner,
      builder: (context, item) {
        return BannerWidget(
          title: item.metadata['title'] as String,
          onClose: () => context
              .presentum<CampaignItem, AppSurface, CampaignVariant>()
              .markDismissed(item),
        );
      },
    );
  }
}
```

When no item is active for the surface, the outlet renders `SizedBox.shrink()` by default.

## Custom placeholder

Provide a custom widget when nothing is shown:

```dart theme={null}
PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
  surface: AppSurface.homeTopBanner,
  builder: (context, item) => BannerWidget(item),
  placeholderBuilder: (context) => const SizedBox(height: 60),
)
```

## Composition outlets

`PresentumOutlet$Composition` gives you access to both active and queued items:

```dart theme={null}
PresentumOutlet$Composition<CampaignItem, AppSurface, CampaignVariant>(
  surface: AppSurface.homeTopBanner,
  builder: (context, active, queue) {
    return Column(
      children: [
        if (active != null)
          ActiveBannerWidget(active),
        ...queue.map((item) => QueuedBannerWidget(item)),
      ],
    );
  },
)
```

<Info>
  Composition outlets are useful for showing "next up" indicators or carousels.
</Info>

## Multi-surface composition

Combine items from multiple surfaces:

```dart theme={null}
PresentumOutlet$Composition2<
  CampaignItem,
  TipItem,
  AppSurface,
  CampaignVariant,
  TipVariant
>(
  surface1: AppSurface.homeTopBanner,
  surface2: AppSurface.homeTip,
  builder: (context, items1, items2) {
    final allItems = [...items1, ...items2];
    return ListView(
      children: allItems.map((item) => ItemWidget(item)).toList(),
    );
  },
)
```

## Cross-presentum composition with animations

`PresentumOutlet$Composition2` supports combining items from **different Presentum instances** (cross-universe composition), allowing seamless coordination between multiple presentation systems with smooth animated transitions.

### Production example

Here's a real-world outlet that combines campaigns and feature flags, showing the highest-priority item with smooth fade and size transitions:

```dart theme={null}
class BannerOutlet extends StatelessWidget {
  const BannerOutlet({super.key});

  @override
  Widget build(BuildContext context) {
    return PresentumOutlet$Composition2<
      CampaignPresentumItem,
      FeatureItem,
      CampaignSurface,
      CampaignVariant,
      AppSurface,
      AppVariant
    >(
      surface1: CampaignSurface.homeTopBanner,
      surface2: AppSurface.homeHeader,
      resolverMode: OutletGroupMode.custom,
      resolver: (campaignItems, featureItems) {
        // Combine items from both Presentum instances
        final allItems = <PresentumItem>[...campaignItems, ...featureItems]
          ..sort((a, b) => b.priority.compareTo(a.priority));

        if (allItems.isEmpty) {
          return <PresentumItem>[];
        }

        // Show only the highest-priority item
        return [allItems.first];
      },
      compositeBuilder: (context, items) {
        return FadeSizeTransitionSwitcher(
          isForwardMove: true,
          child: switch (items.firstOrNull) {
            CampaignPresentumItem(:final surface) => CampaignOutlet(
              key: ValueKey('campaign_banner_$surface'),
              surface: surface,
              padding: const EdgeInsets.symmetric(vertical: 16),
            ),
            FeatureItem() => const NewYearBanner(
              key: ValueKey('new_year_banner'),
              padding: EdgeInsets.symmetric(vertical: 16),
            ),
            _ => const SizedBox.shrink(key: ValueKey('empty_banner')),
          },
        );
      },
    );
  }
}
```

### How it works

1. **Cross-universe composition**: Combines items from two separate Presentum instances:

   * Campaign presentations (`CampaignPresentumItem`)
   * Feature flag presentations (`FeatureItem`)

2. **Priority resolution**: The resolver merges both lists and sorts by priority, selecting only the highest-priority item to display

3. **Animated transitions**: `FadeSizeTransitionSwitcher` provides smooth animations when:
   * A campaign is dismissed and a feature banner appears
   * A feature is disabled and a campaign takes its place
   * Items change priority dynamically

### Smooth transition widget

The `FadeSizeTransitionSwitcher` combines fade and size animations:

```dart theme={null}
class FadeSizeTransitionSwitcher extends StatelessWidget {
  const FadeSizeTransitionSwitcher({
    required this.child,
    this.sizeDuration = const Duration(milliseconds: 300),
    this.fadeDuration = const Duration(milliseconds: 300),
    this.isForwardMove = true,
    super.key,
  });

  final Widget child;
  final Duration sizeDuration;
  final Duration fadeDuration;
  final bool isForwardMove;

  @override
  Widget build(BuildContext context) {
    return AnimatedSize(
      alignment: Alignment.topLeft,
      duration: sizeDuration,
      curve: Curves.fastOutSlowIn,
      child: AnimatedSwitcher(
        duration: fadeDuration,
        layoutBuilder: (currentChild, previousChildren) {
          return Stack(
            clipBehavior: Clip.none,
            children: [
              // Stack previous children during transition
              ...previousChildren.map(
                (child) => Positioned(left: 0, right: 0, child: child),
              ),
              if (currentChild != null) currentChild,
            ],
          );
        },
        transitionBuilder: (child, animation) {
          final isIncoming = child.key == this.child.key;

          if (isIncoming) {
            // Fade in new content
            return FadeTransition(opacity: animation, child: child);
          } else {
            // Fade out old content
            return FadeTransition(
              opacity: Tween(begin: 1.0, end: 0.0).animate(animation),
              child: child,
            );
          }
        },
        child: child,
      ),
    );
  }
}
```

### Key benefits

<CardGroup cols={2}>
  <Card title="Seamless coordination" icon="link">
    Multiple Presentum instances can compete for the same UI slot without conflicts. The outlet handles priority resolution automatically.
  </Card>

  {" "}

  <Card title="Smooth transitions" icon="wand-magic-sparkles">
    When one banner is dismissed, the next highest-priority item smoothly animates
    in with fade and size transitions.
  </Card>

  {" "}

  <Card title="Declarative priority" icon="ranking-star">
    Priority logic is centralized in the resolver. Guards don't need to know about
    other presentation types.
  </Card>

  <Card title="Type-safe composition" icon="shield-check">
    Despite combining different item types, the outlet remains type-safe with pattern matching.
  </Card>
</CardGroup>

<Tip>
  Use unique `ValueKey`s for each widget variant to ensure `AnimatedSwitcher`
  correctly animates between different content types.
</Tip>

[See full production example →](https://github.com/itsezlife/presentum/blob/master/example/lib/src/main/widgets/banner_outlet.dart)

## Popup hosts

For dialogs and overlays, use a **popup host** that watches a surface and shows dialogs:

```dart theme={null}
class CampaignPopupHost extends StatefulWidget {
  const CampaignPopupHost({required this.child, super.key});

  final Widget child;

  @override
  State<CampaignPopupHost> createState() => _CampaignPopupHostState();
}

class _CampaignPopupHostState extends State<CampaignPopupHost>
    with
        PresentumActiveSurfaceItemObserverMixin<
          CampaignPresentumItem,
          CampaignSurface,
          CampaignVariant,
          CampaignPopupHost
        >,
        PresentumPopupSurfaceStateMixin<
          CampaignPresentumItem,
          CampaignSurface,
          CampaignVariant,
          CampaignPopupHost
        > {

  @override
  CampaignSurface get surface => CampaignSurface.popup;

  @override
  bool get ignoreDuplicates => true;

  @override
  Widget build(BuildContext context) => widget.child;

  @override
  Future<PopupPresentResult> present(CampaignPresentumItem entry) async {
    if (!mounted) return PopupPresentResult.notPresented;

    // Record impression
    final campaigns = context.campaignsPresentum;
    await campaigns.markShown(entry);

    if (!mounted) return PopupPresentResult.notPresented;

    final factory = campaignsPresentationWidgetFactory;

    final fullscreenDialog =
        entry.option.variant == CampaignVariant.fullscreenDialog;

    // Result is true if dismissed by user, null if dismissed by system
    final result = await showDialog<bool?>(
      context: context,
      builder: (context) => InheritedPresentum.value(
        value: campaigns,
        child: InheritedPresentumItem<
          CampaignPresentumItem,
          CampaignSurface,
          CampaignVariant
        >(
          item: entry,
          child: factory.buildPopup(context, entry),
        ),
      ),
      barrierDismissible: false,
      fullscreenDialog: fullscreenDialog,
    );

    return result == true
        ? PopupPresentResult.userDismissed
        : PopupPresentResult.systemDismissed;
  }
}
```

[See production popup host ->](https://github.com/itsezlife/presentum/blob/master/example/lib/src/campaigns/presentum/widgets/campaign_popup_host.dart)

<Tip>
  Use the `PresentumPopupSurfaceStateMixin` for automatic dialog lifecycle
  management. It handles showing, dismissing, and queueing popups automatically.
</Tip>

## Accessing Presentum

Inside outlets, access the Presentum instance via context:

```dart theme={null}
// Short form
final presentum = context.presentum<CampaignItem, AppSurface, CampaignVariant>();

// Long form
final presentum = Presentum.of<CampaignItem, AppSurface, CampaignVariant>(context);

// Mark events
presentum.markShown(item);
presentum.markDismissed(item);
presentum.markConverted(item);
```

## Inherited widgets

### InheritedPresentumItem

Outlets automatically wrap builders with `InheritedPresentumItem`, giving child widgets access to the current item:

```dart theme={null}
// Inside outlet builder
widget.builder(context, item)

// Wrapped automatically with:
InheritedPresentumItem<TItem, S, V>(
  item: item,
  child: widget.builder(context, item),
)
```

Access the item in descendants:

```dart theme={null}
class MyCampaignButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get item from ancestor outlet
    final item = context.presentumItem<CampaignItem, AppSurface, CampaignVariant>();

    return ElevatedButton(
      onPressed: () => context
          .presentum<CampaignItem, AppSurface, CampaignVariant>()
          .markConverted(item),
      child: const Text('Learn More'),
    );
  }
}
```

<Warning>
  If you create custom observer widgets (not using `PresentumOutlet`), wrap descendants with `InheritedPresentumItem` so they can access the item:

  ```dart theme={null}
  class MyCustomObserver extends StatefulWidget {
    @override
    Widget build(BuildContext context) {
      final item = _getCurrentItem();

      // Wrap child so descendants can access item
      return InheritedPresentumItem(
        item: item,
        child: MyWidget(),
      );
    }
  }
  ```

  Alternatively, pass the item down manually if you prefer explicit props.
</Warning>

## Outlet patterns

### Conditional rendering

```dart theme={null}
PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
  surface: AppSurface.homeTopBanner,
  builder: (context, item) {
    return switch (item.variant) {
      CampaignVariant.banner => BannerWidget(item),
      CampaignVariant.inline => InlineWidget(item),
      CampaignVariant.dialog => DialogWidget(item),
    };
  },
)
```

### With animations

```dart theme={null}
PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
  surface: AppSurface.homeTopBanner,
  builder: (context, item) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0.0, end: 1.0),
      duration: const Duration(milliseconds: 300),
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.translate(
            offset: Offset(0, 20 * (1 - value)),
            child: child,
          ),
        );
      },
      child: BannerWidget(item),
    );
  },
)
```

### Async loading

```dart theme={null}
PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
  surface: AppSurface.homeTopBanner,
  builder: (context, item) {
    final imageUrl = item.metadata['imageUrl'] as String?;

    return FutureBuilder<ui.Image>(
      future: _loadImage(imageUrl),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return BannerWithImage(item, snapshot.data!);
        }
        return const CircularProgressIndicator();
      },
    );
  },
)
```

## Best practices

<AccordionGroup>
  <Accordion title="No business logic in outlets">
    Don't check eligibility, fetch data, or make decisions in outlets. That belongs in guards.

    **Bad:**

    ```dart theme={null}
    builder: (context, item) {
      if (userIsPremium) return SizedBox.shrink(); // ❌
      return BannerWidget(item);
    }
    ```

    **Good:**

    ```dart theme={null}
    // In guard
    if (userIsPremium) return state; // Don't add item

    // In outlet
    builder: (context, item) => BannerWidget(item); // ✅
    ```
  </Accordion>

  <Accordion title="Keep builders lightweight">
    Outlets rebuild when state changes. Keep builders fast and simple.

    ```dart theme={null}
    // ✅ Good
    builder: (context, item) => BannerWidget(item)

    // ❌ Bad
    builder: (context, item) {
      // Complex calculations, API calls, etc.
      final data = await fetchMoreData(); // Don't do this
      return ComplexWidget(data);
    }
    ```
  </Accordion>

  <Accordion title="Use InheritedPresentumItem for deep trees">
    When you have complex widget hierarchies, use the inherited item instead of prop drilling:

    ```dart theme={null}
    builder: (context, item) {
      return ComplexLayout(
        header: Header(), // Can access item via context
        body: Body(),     // Can access item via context
        footer: Footer(), // Can access item via context
      );
    }
    ```
  </Accordion>
</AccordionGroup>

## Common patterns

### Banner outlet

```dart theme={null}
class BannerOutlet extends StatelessWidget {
  const BannerOutlet({super.key});

  @override
  Widget build(BuildContext context) {
    return PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
      surface: AppSurface.homeTopBanner,
      builder: (context, item) {
        return Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.purple, Colors.blue],
            ),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      item.metadata['title'] as String,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      item.metadata['message'] as String,
                      style: const TextStyle(
                        color: Colors.white70,
                        fontSize: 14,
                      ),
                    ),
                  ],
                ),
              ),
              IconButton(
                icon: const Icon(Icons.close, color: Colors.white),
                onPressed: () => context
                    .presentum<CampaignItem, AppSurface, CampaignVariant>()
                    .markDismissed(item),
              ),
            ],
          ),
        );
      },
    );
  }
}
```

### Card outlet with actions

```dart theme={null}
PresentumOutlet<CampaignItem, AppSurface, CampaignVariant>(
  surface: AppSurface.profileAlert,
  builder: (context, item) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              item.metadata['title'] as String,
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(item.metadata['description'] as String),
            const SizedBox(height: 16),
            Row(
              children: [
                TextButton(
                  onPressed: () => context
                      .presentum<CampaignItem, AppSurface, CampaignVariant>()
                      .markDismissed(item),
                  child: const Text('Dismiss'),
                ),
                const Spacer(),
                ElevatedButton(
                  onPressed: () => context
                      .presentum<CampaignItem, AppSurface, CampaignVariant>()
                      .markConverted(item),
                  child: const Text('Take Action'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  },
)
```

## Next steps

<CardGroup cols={2}>
  <Card title="Building outlets guide" icon="tv" href="/guides/building-outlets">
    Step-by-step outlet creation
  </Card>

  {" "}

  <Card title="Popup hosts" icon="window-maximize" href="/advanced/popup-hosts">
    Advanced dialog management
  </Card>

  {" "}

  <Card title="Auto-tracking" icon="eye" href="/features/auto-tracking">
    Automatic impression tracking
  </Card>

  <Card title="Production example" icon="code" href="https://github.com/itsezlife/presentum/blob/master/example/lib/src/campaigns/presentum/widgets/campaign_popup_host.dart">
    See real popup host
  </Card>
</CardGroup>
