TFN Design System

TFN's design system building blocks for Flutter applications. The package pairs Figma-generated design tokens (colours, spacing, corners, shapes, typography) with a library of branded UI components, so every TFN app renders the same visual language without copy-pasting styles between projects.

This is the foundation used by the TFN Transport apps (tfn_app_ui, tfn_platform_ui). Brand-specific token sets (TFN, KiloMita) are supplied by the brand theme packages that consume this one.

Contents

Installing

dependencies:
  tfn_design_system: ^0.9.0

It depends only on flutter and google_fonts.

Importing

Each layer of the system has its own barrel file — import the layer you need:

import 'package:tfn_design_system/components/components.dart';   // all UI components
import 'package:tfn_design_system/components/inputs/index.dart'; // just the inputs
import 'package:tfn_design_system/theme/brand_theme_scope.dart'; // BrandThemeScope
import 'package:tfn_design_system/tokens/index.dart';            // token classes
import 'package:tfn_design_system/foundation/index.dart';        // base primitives
import 'package:tfn_design_system/features/features.dart';       // sign-in scaffold
import 'package:tfn_design_system/data/data.dart';               // mock repositories
import 'package:tfn_design_system/utils/responsive_utils.dart';  // breakpoints

Quick start

Everything keys off a BrandTokens instance — the strongly typed bundle of one brand's colours, spacing, shapes, typography and assets. Brand packages (e.g. tfn_theme, kilomita_theme) construct the BrandTokens for their brand; your app wraps its root widget in a BrandThemeScope so every component can read them:

import 'package:flutter/material.dart';
import 'package:tfn_design_system/theme/brand_theme_scope.dart';
import 'package:tfn_theme/tfn_theme.dart'; // your brand theme package

void main() {
  runApp(
    BrandThemeScope(
      tokens: TfnBrand.tfnBrand,   // the brand's BrandTokens
      child: const Application(),
    ),
  );
}

Inside any widget below the scope, read the tokens from context:

@override
Widget build(BuildContext context) {
  final theme = BrandThemeScope.of(context);   // BrandTokens
  return Container(
    padding: EdgeInsets.all(theme.spacing.spaceMd1),
    decoration: BoxDecoration(
      color: theme.colour.surface,
      borderRadius: BorderRadius.circular(theme.shapes.cornerMedium),
    ),
    child: Text(
      'Hello',
      style: theme.typography.bodyMediumRegular.toTextStyle(
        color: theme.componentColours.textBlack,
      ),
    ),
  );
}

BrandThemeScope.maybeOf(context) returns null instead of asserting when no scope is present (useful in tests and Widgetbook).

Theme building utilities (turning BrandTokens into a Material 3 ThemeData) live in the consuming repo's theme_base package — this package deliberately only owns the tokens and the scope.

Design tokens

BrandTokens groups the generated token catalogues:

Property Type Contents
colour ColourTokens Material-style roles: primary, secondary, tertiary, surface, outline, outlineVariant, error, warning, success, information, containers, …
colourPrimitives ColourPrimitivesTokens The raw palette the roles are built from
componentColours ComponentColourTokens Component-specific colours: textBlack, textGrey, iconPrimary, chipSelectedContainer, navBarActiveLabel, button containers, …
spacing SpacingTokens 19 levels from spaceNone through spaceXs1/2, spaceSm1/2, spaceMd1–4, spaceLg1–5, spaceXl1/2, spaceXxl1/2 to spaceFull
shapes ShapeTokens Corner radii by purpose: cornerChips, cornerRounded, cornerTooltip, cornerSmall/Medium/Large/Xl, …
typography TypographyTokens 24 presets (titleLarge, headlineMedium, bodyMediumRegular, label, …); call .toTextStyle(color: ...) to get a TextStyle
assets BrandAssets Brand imagery: banner, authLogo

Typical in-widget usage:

final theme = BrandThemeScope.of(context);

EdgeInsets.symmetric(horizontal: theme.spacing.spaceSm1);
BorderRadius.circular(theme.shapes.cornerChips);
theme.typography.titleLarge.toTextStyle(color: theme.componentColours.textBlack);

The classes in foundation/ (BaseSpacing, BaseShapes, BaseFonts, BaseSizes, …) are the static primitives the generated tokens are assembled from — consumers normally use the tokens, not the foundation, directly.

Buttons

AppButton covers the four Material button styles behind one API:

import 'package:tfn_design_system/components/components.dart';

AppButton(
  label: 'Save',
  style: AppButtonStyle.filled,        // elevated | filled | outline | text
  leadingIcon: const Icon(Icons.check),
  onPressed: () => save(),
);

AppButton(label: 'Cancel', style: AppButtonStyle.text, onPressed: cancel);
AppButton(label: 'Disabled', enabled: false, onPressed: null);

Inputs

import 'package:tfn_design_system/components/inputs/index.dart';

TextInput

The base text field — label, placeholder, helper/error text, icons and validation:

TextInput(
  label: 'Registration number',
  placeholder: 'e.g. ND 123-456',
  required: true,
  helperText: 'As shown on the licence disc',
  validator: (v) => (v == null || v.isEmpty) ? 'Required' : null,
  leadingIcon: const Icon(Icons.directions_car),
  onChanged: (v) => registration = v,
);

PasswordInput

Wraps TextInput with an obscure-text visibility toggle:

PasswordInput(
  controller: passwordController,
  validator: (v) => (v == null || v.isEmpty) ? 'Password is required' : null,
  textInputAction: TextInputAction.done,
);

SearchInput

A rounded search field with a built-in clear button and optional loading state:

SearchInput(
  placeholder: 'Search vehicles',
  onChanged: (query) => vm.updateSearchQuery(query),
  onClear: () => vm.clearSearch(),
  isLoading: vm.isSearching,
);

CellphoneInput

Phone number entry with a country-code selector (South Africa, Mozambique, Zimbabwe, Namibia, Zambia, Botswana):

CellphoneInput(
  controller: cellController,
  required: true,
  onCountryChanged: (country) => dialCode = country.dialCode,
);

StandardDropdownMenu

A searchable, filterable dropdown with clearable selection:

StandardDropdownMenu(
  placeholderText: 'Select a depot',
  options: depots.map((d) => DropdownOption(label: d.name, value: d.id)).toList(),
  onSelected: (option) => depotId = option?.value,
);

Form controls

AppCheckbox(
  label: 'I agree to the terms',
  value: agreed,                       // tri-state: false → true → null
  onChanged: (v) => setState(() => agreed = v),
);

AppRadio(
  label: 'Owner driver',
  value: isOwnerDriver,
  onChanged: (v) => setState(() => isOwnerDriver = v),
);

AppSwitch(
  label: 'Notifications',
  value: notificationsOn,
  showIcon: true,                      // check / cross inside the thumb
  onChanged: (v) => setState(() => notificationsOn = v),
);

Cards

ContainerCard(
  child: VehicleSummary(vehicle),
);

MessageCard(
  type: MessageType.warning,           // success | error | warning | info
  title: 'Sync pending',
  message: 'Changes will upload when you are back online.',
  onDismiss: () => vm.dismissBanner(),
);

Chips

// Note: the design system's FilterChip shares its name with Material's —
// import with a prefix if you use both in one file.
FilterChip(
  label: 'In progress',
  isSelected: filter == Status.inProgress,
  trailingNumber: inProgressCount,
  onSelected: (selected) => vm.setFilter(Status.inProgress, selected),
);

SelectionChip(
  label: 'South Africa',
  isSelected: true,
  leadingIcon: const Text('🇿🇦'),
  onDeleted: () => vm.removeCountry('ZA'),
);

Bottom navigation bar

AppNavigationBar(
  items: const [
    AppNavItem(icon: Icons.dashboard, label: 'Dashboard'),
    AppNavItem(icon: Icons.directions_car, label: 'Vehicles'),
    AppNavItem(icon: Icons.person, label: 'Profile'),
  ],
  selectedIndex: index,
  onChanged: (i) => setState(() => index = i),
);

Tabs

AppTabs(
  tabs: const [
    AppTab(label: 'Active'),
    AppTab(label: 'History'),
  ],
  style: AppTabStyle.primary,          // primary | secondary
  selectedIndex: tab,
  onChanged: (i) => setState(() => tab = i),
);
AppMenuItem(
  label: 'Settings',
  leadingIcon: Icons.settings,
  supportingText: 'Account and preferences',
  showDivider: true,
  onTap: () => openSettings(),
);

Feedback & indicators

const Loader();                                    // animated brand loader, 40×40

AppLinearProgressIndicator(progress: 0.6);          // 0..1

AppCircularProgressIndicator(
  progress: 0.6,
  showText: true,
  loadingText: 'Uploading…',
);

NotificationBadge(count: 12);                       // caps display at 99+
OnlineStatusBadge(status: OnlineStatusType.online); // online | offline | busy
AppTooltip(message: 'Last synced 5 minutes ago');
UserAvatar(name: user.fullName);                    // null shows 'Deleted User'

Layout shells

AuthShell

The branded authentication layout: full-screen brand banner (from BrandTokens.assets.banner) with a centred, responsively constrained content area (mobile / tablet / desktop breakpoints handled for you):

AuthShell(
  child: SignInCard(/* your form */),
);

ViewShell

A standard content wrapper with consistent padding, plus ready-made loading and error states:

ViewShell(child: VehicleList(vehicles));
const ViewShell.loading();
const ViewShell.error();

Sign-in feature

features/signin ships a complete sign-in scaffold — SignInScreen renders a loading / error / data view around a SignInCard (username + password + submit) inside AuthShell:

import 'package:tfn_design_system/features/features.dart';

SignInScreen(
  formKey: _formKey,
  username: _usernameController,
  password: _passwordController,
  handleLogin: () async => await auth.signIn(),
  errorMessage: vm.errorMessage,
);

The accompanying data/ library (User, Vehicle, UserRepository, VehicleRepository, mocks.dart) provides mock models and repositories intended for prototyping and Widgetbook stories — production apps wire up their own services.

Utilities

ResponsiveUtil exposes the breakpoints used throughout the system:

import 'package:tfn_design_system/utils/responsive_utils.dart';

ResponsiveUtil.screenWidth(context);
ResponsiveUtil.isMobileScreen(context);   // ≤ 600
ResponsiveUtil.isTabletScreen(context);   // 800 – 1200
ResponsiveUtil.isLargeScreen(context);    // ≥ 1200

ResponsiveImage is a thin Image.network wrapper used by the shells:

ResponsiveImage(url: theme.assets.banner ?? '', fit: BoxFit.cover);

Regenerating tokens from Figma

All token classes are generated from the Figma variable export — treat them as read-only and never hand-edit them.

  1. Export the variables from Figma and save them as tokens/figma_export.json (the export contains all brands).

  2. Run the generator:

    dart run scripts/generate_tokens.dart
    # or with a custom export path:
    dart run scripts/generate_tokens.dart path/to/export.json
    
  3. Commit the regenerated Dart files so downstream apps and Widgetbook stay in sync with Figma.

See tokens/README.md and lib/theme/README.md for the full guidelines.

Libraries

components/auth_shell
components/button/button
components/cards/cards
components/cards/collapsible_card
components/cards/container_card
components/cards/dashboard_card
components/cards/debit_order_message_card
components/cards/detail_card
components/cards/message_card
components/checkbox
components/chips/filter_chip
components/chips/selection_chip
components/chips/status_filter_chip
components/chips/status_filter_chips
components/components
components/inputs/cellphone
components/inputs/date_field
components/inputs/file
components/inputs/index
components/inputs/password
components/inputs/text
components/list_item
components/loader
components/notification_badge
components/online_status
components/progress_indicator
components/radio
components/responsive_image
components/switch
components/tabs
components/tooltip
components/user_avatar
components/view_shell
data/data
data/mocks
data/user/user
data/user/user_repository
data/vehicles/vehicle
data/vehicles/vehicle_repository
features/features
features/signin/async_value
features/signin/components/components
features/signin/components/signin_card
features/signin/signin
features/signin/signin_screen
features/signin/state/state
features/signin/state/user_provider
features/signin/views/signin_data_view
features/signin/views/signin_error_view
features/signin/views/signin_loading_view
features/signin/views/views
foundation/corner
foundation/elevation
foundation/fonts
foundation/index
foundation/shapes
foundation/sizes
foundation/spacing
foundation/text_styles
theme/brand_theme
Theme scope widget for accessing BrandTokens in the widget tree.
theme/brand_theme_scope
tokens/brand_assets
tokens/brand_tokens
tokens/colour_primitives_tokens
tokens/colour_tokens
tokens/component_colour_tokens
tokens/corner_tokens
tokens/elevation_tokens
tokens/index
tokens/shape_tokens
tokens/size_primitive_tokens
tokens/spacing_tokens
tokens/type_primitives
tokens/typography_tokens
utils/responsive_utils