tfn_design_system 0.3.2 copy "tfn_design_system: ^0.3.2" to clipboard
tfn_design_system: ^0.3.2 copied to clipboard

TFN's Design System building blocks for Flutter applications.

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.

0
likes
120
points
180
downloads

Documentation

API reference

Publisher

verified publishercodecollective.com

Weekly Downloads

TFN's Design System building blocks for Flutter applications.

Repository

License

BSD-3-Clause (license)

Dependencies

flutter, google_fonts

More

Packages that depend on tfn_design_system