safaeh 0.2.1 copy "safaeh: ^0.2.1" to clipboard
safaeh: ^0.2.1 copied to clipboard

Adaptive sheets, dialogs, camera/QR chrome, page index, and sidenav for Flutter.

Safaeh

Safaeh - صفائح

safaeh
Adaptive sheets, camera / QR chrome, page index, and sidenav for Flutter —
host app keeps i18n, routing, and the camera plugin.

Live demo

pub.dev repo flutter license

Live demo — open the example catalog in the browser
zyzto.github.io/Safaeh

Live demo · Install · Quick start · Widgets · Features · Host app · Example · Changelog · Versioning · Host integration · العربية

The name safaeh comes from Arabic صفائح (ṣafāʾiḥ): sheets / plates — plural of صفيحة (ṣafīḥa).


Why #

Flutter apps accumulate one-off bottom sheets, dialogs, rails, and camera overlays. Then you need:

  • the same route to be a phone sheet and a tablet dialog, and morph when the viewport crosses the breakpoint
  • chrome that honors MediaQuery.disableAnimationsOf
  • no easy_localization, Riverpod, go_router, or mobile_scanner inside the package

Safaeh is that chrome layer. Used in Hisab.

On pub.dev: safaeh · Repo: Zyzto/Safaeh.


Widgets #

Card picker — live demo Confirm sheet — live demo Option tiles — live demo

Card picker · Confirm · Option tiles — try them in the live demo

Sidenav rail — live demo

Captured with widgets_to_image (cd example && flutter test test/widget_images_test.dart).


Features at a glance #

Area What you get
Sheets showSafaeh morphs phone sheet ↔ tablet dialog; showSafaehPicker / SafaehOption (cards, enabled); showSafaehTilePicker / SafaehTileOption (list rows, search); showSafaehMultiTilePicker (multi-select); showSafaehConfirm, showSafaehTextInput, SafaehStatusBody, buildSafaehSheetShell, SafaehOptionList, SafaehOptionTile
Dialog showSafaehDialog centered panel with optional railWidthOf
Theme SafaehTheme / SafaehThemeData for breakpoint, motion, radius, rail widths, camera compact height, contentMaxWidth; copyWith
Motion safaehResolvedMotion zeros durations when animations are disabled
Nav SafaehSidenav temporary drawer (asDrawer: true) or clipping rail; SafaehFloatingNavBar (same SafaehSidenavDestination)
Page index SafaehPageIndex, overlay, scrollToPageSection, safaehActivePageSectionId (ids + keys only — no .tr() on scroll)
Content safaehBandMetrics, SafaehContentBand, SafaehEndAsideLayout, SafaehContentAlignedAppBar, SafaehContentAlignedFabLocation
Camera showSafaehCameraSheet / SafaehCameraSheetHost paper-roll compact ↔ full
QR chrome SafaehQrScannerOverlay (optional host preview), SafaehQrTopBar, SafaehQrMessageBody, SafaehQrFramePainter
RTL safaehChevronEnd, safaehChevronStart, safaehArrowBack (LTR glyphs; Material matchTextDirection mirrors them)

Core: Flutter Material only. Preview, decode, copy, and navigation stay in the host.


Install #

dependencies:
  safaeh: ^0.2.1

Or:

flutter pub add safaeh

Git tag pin (see VERSIONING.md):

dependencies:
  safaeh:
    git:
      url: https://github.com/Zyzto/Safaeh.git
      ref: v0.2.1
import 'package:safaeh/safaeh.dart';

Current version: 0.2.1.


Quick start #

1. Wrap the app #

SafaehTheme(
  data: const SafaehThemeData(
    tabletBreakpoint: 600,
    dialogMaxWidth: 560,
  ),
  child: MaterialApp(
    home: const MyHome(),
  ),
);

Call-sites can still override breakpoint, motion, and transitions.

2. Adaptive sheet #

await showSafaeh<void>(
  context: context,
  title: 'Rename',
  titleBuilder: (context, style) => Text('Rename', style: style),
  child: const TextField(),
);

Phone: bottom sheet. Tablet+: centered dialog. The same route morphs when width crosses tabletBreakpoint. Pass phonePlacement: SafaehPhoneSheetPlacement.center to grow the phone sheet so the first content center aligns with the phone center (still flush with the bottom).

3. Option picker #

final choice = await showSafaehPicker<int>(
  context: context,
  title: 'How to settle',
  selected: 1,
  options: const [
    SafaehOption(
      value: 1,
      label: 'Minimal',
      subtitle: 'Fewest transfers',
      icon: Icons.bolt_outlined,
    ),
  ],
);

The in-body title hides when the viewport is wide (header title only). SafaehOption.enabled greys the card and ignores taps.

4. Tile picker (list rows) #

final mode = await showSafaehTilePicker<String>(
  context: context,
  title: 'Import mode',
  titleBuilder: (context, style) => Text('Import mode', style: style),
  header: const Text('12 new · 3 updated'),
  selected: 'add',
  options: const [
    SafaehTileOption(
      value: 'add',
      label: 'Add copies',
      subtitle: 'Keeps existing data',
      leading: Icon(Icons.add_circle_outline),
    ),
    SafaehTileOption(
      value: 'replace',
      label: 'Replace',
      enabled: false,
    ),
  ],
);

Uses SafaehOptionList + SafaehOptionTile. Hosts that already wrap showSafaeh can mount SafaehTilePickerBody as the child. Same knobs as other showSafaeh* helpers (railWidthOf, motion, …).

5. Confirm and text input #

Host passes every label. Phone shows cancel in the action row; tablet uses the sheet close control. showSafaehConfirm returns true if confirmed, false if the phone cancel button is pressed, and null if dismissed (tablet close, barrier, or system back). Treat only ok == true as confirmed.

final ok = await showSafaehConfirm(
  context: context,
  title: 'Delete item',
  content: 'This cannot be undone.',
  confirmLabel: 'Delete',
  cancelLabel: 'Cancel',
  isDestructive: true,
  titleBuilder: (context, style) => Text('Delete item', style: style),
);

final name = await showSafaehTextInput(
  context: context,
  title: 'Tag name',
  doneLabel: 'Done',
  cancelLabel: 'Cancel',
  titleBuilder: (context, style) => Text('Tag name', style: style),
);

6. Centered dialog #

await showSafaehDialog<void>(
  context: context,
  railWidthOf: (context) => 0,
  builder: (context) => const Card(child: Text('Hello')),
);

7. Floating nav and content band #

SafaehFloatingNavBar(
  selectedIndex: index,
  onDestinationSelected: (i) => setState(() => index = i),
  destinations: const [
    SafaehSidenavDestination(
      label: 'Home',
      icon: Icons.home_outlined,
      selectedIcon: Icons.home,
    ),
  ],
);

SafaehContentBand(
  aside: const Text('On this page'),
  child: body,
);

SafaehContentBand centers from incoming constraints and hides aside when narrow (SafaehThemeData.isWide). Hosts with a sibling shell rail keep their own leftOffset / bandWidth math and use SafaehEndAsideLayout.

Band metrics for other apps (app bar, FAB, aside):

final metrics = safaehBandMetrics(
  contentAreaWidth: constraints.maxWidth,
  maxWidth: 600,
);

SafaehContentAlignedAppBar(
  leftOffset: metrics.leftOffset,
  bandWidth: metrics.bandWidth,
  title: const Text('Title'),
);

SafaehContentAlignedFabLocation.resolve(
  leftOffset: metrics.leftOffset,
  bandWidth: metrics.bandWidth,
  endFree: metrics.endFree,
  textDirection: Directionality.of(context),
);

See doc/host-integration.md.

8. Camera / QR chrome #

await showSafaehCameraSheet<void>(
  context: context,
  builder: (context, sheet) => MyPreview(
    expanded: sheet.expanded,
    onToggle: sheet.toggleExpanded,
    onClose: sheet.dismiss,
  ),
);

Embed on a route with SafaehCameraSheetHost (omit openAnimation, pass onDismiss). Put SafaehQrScannerOverlay / SafaehQrMessageBody inside that bottom panel — they are full-bleed overlays, not their own sheet. Keep mobile_scanner in the app.


What stays in the host #

Concern Stays in the app
Copy easy_localization, UserText, titleBuilder
Routing go_router, reserved rail width via railWidthOf
Camera mobile_scanner, permissions, SystemChrome orientation lock
State Riverpod / whatever the app already uses
Tiles UserText + optional accent colors on SafaehOptionTile

UI inventory #

Sheets: showSafaeh, SafaehRouteOptions, showSafaehPicker, SafaehOption, SafaehOptionPickerBody, showSafaehTilePicker, showSafaehMultiTilePicker, SafaehTileOption, SafaehTilePickerBody, SafaehTileBuilder, showSafaehConfirm, SafaehConfirmSheet, showSafaehTextInput, SafaehTextInputSheet, SafaehStatusBody, buildSafaehSheetShell, SafaehOptionList, SafaehOptionTile, kSheetContentPadding, kSafaehSheetPadding, SafaehTitleBuilder, SafaehLabelBuilder, safaehTitleFromLabel, safaehPop, SafaehTransition, safaehFadeScale, safaehFade, SafaehPhoneSheetPlacement, safaehPhoneCenterSheetTop

Dialog: showSafaehDialog

Camera: showSafaehCameraSheet, SafaehCameraSheetHost, SafaehCameraSheet, SheetHandleBar, SheetHandleDrag

QR: SafaehQrScannerOverlay, SafaehQrTopBar, SafaehQrMessageBody, SafaehQrFramePainter

Shell: SafaehSidenav, SafaehSidenavDestination, SafaehSidenavProfile, SafaehSidenavAvatar, SafaehFloatingNavBar, SafaehPageIndex, SafaehPageIndexOverlay, scrollToPageSection, safaehActivePageSectionId, safaehBandMetrics, SafaehContentBand, SafaehEndAsideLayout, SafaehContentAlignedAppBar, SafaehContentAlignedFabLocation

Tokens: SafaehTheme, SafaehThemeData, SafaehThemeData.copyWith, safaehResolvedMotion, kSafaehCameraCompactHeightFraction

RTL: safaehChevronEnd, safaehChevronStart, safaehArrowBack


Architecture #

┌─────────────────────────────────────────────────────────────┐
│         Host app (i18n, GoRouter, camera, UserText)         │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│                      SafaehTheme                             │
│   breakpoint · motion · radius · rail · camera fraction     │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│  showSafaeh / picker / confirm / text / dialog              │
│  SafaehSidenav · floating nav · page index · content band   │
│  camera sheet host · QR overlay chrome                      │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│              Flutter Material (no Riverpod)                 │
└─────────────────────────────────────────────────────────────┘

Example #

Live demo — zyzto.github.io/Safaeh

The hosted catalog is the example/ web build. CI deploys it after tests pass on main.

A Riverpod-free catalog lives in example/ — same split as Edadat: catalog.dart (en / ar / ja / zh / es), app.dart (theme + home), and a vertical SafaehContentBand gallery of every public API. Section titles open the standalone demo; wide bands use extra columns on the same page. Language and theme toggles, no mobile_scanner.

The example is package-style (web only in-tree); analyze with:

cd example && flutter pub get && dart analyze --fatal-infos && flutter test

example/test/widget_images_test.dart writes widget PNGs to screenshots/.

To run on a device, generate the other platforms (flutter create . --platforms=android,ios inside example/). Details: example/README.md.

Package tests:

dart analyze --fatal-infos && flutter test

Branding #

The logo wordmark uses Baz (Baz Light) — the same Arabic typeface as Edadat and Siglat. The SVG outlines صــفائح (tatweel after ص) so GitHub renders without loading the font. Baz is not registered as a package font and the OTF is not shipped.


Versioning #

See VERSIONING.md and CHANGELOG.md. Tags are vX.Y.Z and must match pubspec.yaml.


License #

MPL-2.0 — weak copyleft, commercial use allowed. Modified package files stay under MPL; your app can remain closed-source.

0
likes
160
points
0
downloads
screenshot

Documentation

API reference

Publisher

verified publishershenepoy.com

Weekly Downloads

Adaptive sheets, dialogs, camera/QR chrome, page index, and sidenav for Flutter.

Repository (GitHub)
View/report issues

License

MPL-2.0 (license)

Dependencies

flutter

More

Packages that depend on safaeh