ashlarui

Marker

A note in a conversation that nobody said: a day, someone joining, thinking.

Marker, as it looks in an app Marker, as it looks in an app, dark
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';

import 'ui/marker.dart';
import 'ui/spinner.dart';

class MarkerDemo extends StatelessWidget {
  const MarkerDemo({super.key});

  @override
  Widget build(BuildContext context) => const SizedBox(
    width: 400,
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Marker(variant: MarkerVariant.separator, child: Text('Today')),
        SizedBox(height: 24),
        Marker(
          icon: AshlarIcon(AshlarIconsRoundedStroke.userPlus),
          child: Text('Kofi joined the conversation'),
        ),
        SizedBox(height: 24),
        Marker(icon: Spinner(size: 16), child: Text('Searching the docs…')),
        SizedBox(height: 24),
        Marker(variant: MarkerVariant.border, child: Text('Earlier messages')),
      ],
    ),
  );
}

Try its options

Each option its constructor takes, to change and see; the code below it is what you chose, to copy.

Marker with controls, as it looks in an app Marker with controls, as it looks in an app, dark

Add it

In your Flutter project:

$ ashlar add marker

It writes theme.dart, palette.dart and marker.dart into lib/ui, keeping any you already have.

Examples

Other ways to use it, each running live, with its code.

New messages

A line where the unread begin.

New messages, as it looks in an app New messages, as it looks in an app, dark
// New messages: A line where the unread begin.
import 'package:flutter/widgets.dart';

import 'ui/marker.dart';

class MarkerUnreadExample extends StatelessWidget {
  const MarkerUnreadExample({super.key});

  @override
  Widget build(BuildContext context) => const SizedBox(
    width: 420,
    child: Marker(variant: MarkerVariant.separator, child: Text('3 new messages')),
  );
}

API

Read from the source above: each parameter with its type, its default, and what it is for.

MarkerVariantenum

How a Marker sets itself apart.

ValueWhat it is for
plainFaint text, nothing more.
separatorCentred between two lines: a day changing, someone joining.
borderA line under it.

Markerclass

A note in a conversation that nobody said: "Today", "Ama joined", "Thinking…". Faint, with an optional icon.

Parameter Type Default What it is for
childrequired Widget The note: Text('Today').
variant MarkerVariant MarkerVariant.plain Faint text, between two lines, or with a line under it.
icon Widget? 16 square, before the text. Hidden from screen readers: the text says it.

Source

What lands in your app. It is yours from then on: change it as you would your own code, and ashlar add will not write over it.

lib/ui/marker.dart 101 lines
// Ashlar UI: Marker. Copied into your app by `ashlar add marker`; it is yours to change.

import 'package:flutter/widgets.dart';

import 'theme.dart';

/// How a [Marker] sets itself apart.
enum MarkerVariant {
  /// Faint text, nothing more.
  plain,

  /// Centred between two lines: a day changing, someone joining.
  separator,

  /// A line under it.
  border,
}

/// A note in a conversation that nobody said: "Today", "Ama joined", "Thinking…". Faint,
/// with an optional icon.
///
/// ```dart
/// Marker(variant: MarkerVariant.separator, child: Text('Today'))
/// Marker(icon: Spinner(size: 16), child: Text('Searching the docs…'))
/// ```
class Marker extends StatelessWidget {
  const Marker({
    super.key,
    required this.child,
    this.variant = MarkerVariant.plain,
    this.icon,
  });

  /// The note: Text('Today').
  final Widget child;

  /// Faint text, between two lines, or with a line under it.
  final MarkerVariant variant;

  /// 16 square, before the text. Hidden from screen readers: the text says it.
  final Widget? icon;

  @override
  Widget build(BuildContext context) {
    final t = AshlarTheme.of(context);
    final c = t.colors;
    final text = DefaultTextStyle.merge(
      style: t.text.sm.copyWith(color: c.mutedForeground),
      textAlign: variant == MarkerVariant.separator ? TextAlign.center : null,
      child: child,
    );
    final content = [
      if (icon != null) ...[
        ExcludeSemantics(
          child: IconTheme.merge(
            data: IconThemeData(color: c.mutedForeground, size: 16),
            child: SizedBox.square(dimension: 16, child: Center(child: icon)),
          ),
        ),
        const SizedBox(width: 8),
      ],
    ];
    Widget line() => Expanded(child: Container(height: 1, color: c.border));
    final body = switch (variant) {
      // The text takes what it needs, up to most of the width; the lines share the rest.
      MarkerVariant.separator => LayoutBuilder(
        builder: (context, box) => Row(
          children: [
            line(),
            const SizedBox(width: 12),
            ...content,
            ConstrainedBox(
              constraints: BoxConstraints(maxWidth: box.maxWidth * .7),
              child: text,
            ),
            const SizedBox(width: 12),
            line(),
          ],
        ),
      ),
      _ => Row(
        children: [
          ...content,
          Expanded(child: text),
        ],
      ),
    };
    return ConstrainedBox(
      constraints: const BoxConstraints(minHeight: 16),
      child: variant == MarkerVariant.border
          ? Container(
              padding: const EdgeInsets.only(bottom: 8),
              decoration: BoxDecoration(
                border: Border(bottom: BorderSide(color: c.border)),
              ),
              child: body,
            )
          : body,
    );
  }
}