tfn_design_system 0.3.2
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
- Importing
- Quick start — provide tokens with
BrandThemeScope - Design tokens — what's in
BrandTokensand how to use it - Buttons
- Inputs — text, password, search, cellphone, dropdown
- Form controls — checkbox, radio, switch
- Cards
- Chips
- Navigation — navigation bar, tabs, menu items
- Feedback & indicators — loader, progress, badges, tooltip
- Layout shells —
AuthShell,ViewShell - Sign-in feature
- Utilities
- Regenerating tokens from Figma
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
BrandTokensinto a Material 3ThemeData) live in the consuming repo'stheme_basepackage — 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'),
);
Navigation #
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),
);
Menu items #
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.
-
Export the variables from Figma and save them as
tokens/figma_export.json(the export contains all brands). -
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 -
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.