Phase 4 Stage 0: design system foundation (client_app)
- HaloTokens, HaloSpacing, HaloRadius, HaloMotion, HaloShadows (warm palette; calm/playful stubbed for phase 5). - Bundled Bricolage Grotesque, Poppins, JetBrains Mono (~1.2 MB total, OFL). - haloThemeData() wired into MaterialApp.router with Figma-aligned text scale, pill ElevatedButton, 64px input height, 24px-corner BottomSheet, dark pill SnackBar. - Halo* widget primitives: Button, Orb, StepDots, BottomSheet, Popup, Snackbar, Chip. - Dev-only /_theme_preview route gated by --dart-define=THEME_PREVIEW=true for visual reference during stages 2-8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
289
client_app/lib/core/theme/_preview.dart
Normal file
289
client_app/lib/core/theme/_preview.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'halo_tokens.dart';
|
||||
import 'widgets/widgets.dart';
|
||||
|
||||
const bool kThemePreviewEnabled = bool.fromEnvironment(
|
||||
'THEME_PREVIEW',
|
||||
defaultValue: false,
|
||||
);
|
||||
|
||||
class ThemePreviewScreen extends StatefulWidget {
|
||||
const ThemePreviewScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ThemePreviewScreen> createState() => _ThemePreviewScreenState();
|
||||
}
|
||||
|
||||
class _ThemePreviewScreenState extends State<ThemePreviewScreen> {
|
||||
final Set<String> _selectedChips = {'gak nyenyak'};
|
||||
bool _disablePrimary = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = Theme.of(context).textTheme;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Halo theme preview')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(HaloSpacing.s20),
|
||||
children: [
|
||||
_section('Typography'),
|
||||
Text('display large 36/700', style: text.displayLarge),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
Text('title large 22/700', style: text.titleLarge),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
Text(
|
||||
'body medium 15/400 — Poppins',
|
||||
style: text.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
Text(
|
||||
'label small 10/600 — caption tracking',
|
||||
style: text.labelSmall,
|
||||
),
|
||||
_divider(),
|
||||
|
||||
_section('Color tokens'),
|
||||
const Wrap(
|
||||
spacing: HaloSpacing.s8,
|
||||
runSpacing: HaloSpacing.s8,
|
||||
children: [
|
||||
_Swatch('brand', HaloTokens.brand),
|
||||
_Swatch('brandDark', HaloTokens.brandDark),
|
||||
_Swatch('brandSoft', HaloTokens.brandSoft),
|
||||
_Swatch('accent', HaloTokens.accent),
|
||||
_Swatch('mint', HaloTokens.mint),
|
||||
_Swatch('lilac', HaloTokens.lilac),
|
||||
_Swatch('success', HaloTokens.success),
|
||||
_Swatch('danger', HaloTokens.danger),
|
||||
_Swatch('ink', HaloTokens.ink, label: Colors.white),
|
||||
_Swatch('inkSoft', HaloTokens.inkSoft, label: Colors.white),
|
||||
],
|
||||
),
|
||||
_divider(),
|
||||
|
||||
_section('HaloButton'),
|
||||
Wrap(
|
||||
spacing: HaloSpacing.s8,
|
||||
runSpacing: HaloSpacing.s8,
|
||||
children: [
|
||||
HaloButton(
|
||||
label: 'primary md',
|
||||
onPressed: _disablePrimary ? null : () {},
|
||||
),
|
||||
HaloButton(
|
||||
label: 'secondary',
|
||||
variant: HaloButtonVariant.secondary,
|
||||
onPressed: () {},
|
||||
),
|
||||
HaloButton(
|
||||
label: 'ghost',
|
||||
variant: HaloButtonVariant.ghost,
|
||||
onPressed: () {},
|
||||
),
|
||||
HaloButton(
|
||||
label: 'small',
|
||||
size: HaloButtonSize.sm,
|
||||
onPressed: () {},
|
||||
),
|
||||
HaloButton(
|
||||
label: 'large with icon',
|
||||
size: HaloButtonSize.lg,
|
||||
icon: const Icon(Icons.send_rounded),
|
||||
onPressed: () {},
|
||||
),
|
||||
const HaloButton(
|
||||
label: 'disabled',
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
Row(
|
||||
children: [
|
||||
Switch(
|
||||
value: _disablePrimary,
|
||||
onChanged: (v) => setState(() => _disablePrimary = v),
|
||||
),
|
||||
const Text('disable primary'),
|
||||
],
|
||||
),
|
||||
_divider(),
|
||||
|
||||
_section('HaloOrb'),
|
||||
Wrap(
|
||||
spacing: HaloSpacing.s12,
|
||||
runSpacing: HaloSpacing.s12,
|
||||
children: List.generate(
|
||||
6,
|
||||
(i) => HaloOrb(seed: i, label: 'ABCDEF'[i]),
|
||||
),
|
||||
),
|
||||
_divider(),
|
||||
|
||||
_section('HaloStepDots'),
|
||||
for (int c = 1; c <= 4; c++) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: HaloSpacing.s8),
|
||||
child: HaloStepDots(total: 4, current: c),
|
||||
),
|
||||
],
|
||||
_divider(),
|
||||
|
||||
_section('HaloChip (ESP-style multi-select)'),
|
||||
Wrap(
|
||||
spacing: HaloSpacing.s8,
|
||||
runSpacing: HaloSpacing.s8,
|
||||
children: [
|
||||
for (final t in const [
|
||||
'gak nyenyak',
|
||||
'overthinking',
|
||||
'putus',
|
||||
'kerjaan',
|
||||
'keluarga',
|
||||
'sendiri',
|
||||
])
|
||||
HaloChip(
|
||||
label: t,
|
||||
selected: _selectedChips.contains(t),
|
||||
onTap: () => setState(() {
|
||||
if (!_selectedChips.add(t)) _selectedChips.remove(t);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
_divider(),
|
||||
|
||||
_section('HaloBottomSheet / HaloPopup / HaloSnackbar'),
|
||||
Wrap(
|
||||
spacing: HaloSpacing.s8,
|
||||
runSpacing: HaloSpacing.s8,
|
||||
children: [
|
||||
HaloButton(
|
||||
label: 'show bottom sheet',
|
||||
variant: HaloButtonVariant.secondary,
|
||||
onPressed: () => HaloBottomSheet.show<void>(
|
||||
context,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('halo bestie', style: text.titleLarge),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
Text(
|
||||
'mau verif nomor dulu, atau ngobrol anonim?',
|
||||
style: text.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s24),
|
||||
HaloButton(
|
||||
label: 'verif nomor',
|
||||
fullWidth: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
HaloButton(
|
||||
label: 'lanjut anonim',
|
||||
variant: HaloButtonVariant.ghost,
|
||||
fullWidth: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
HaloButton(
|
||||
label: 'show popup',
|
||||
variant: HaloButtonVariant.secondary,
|
||||
onPressed: () => HaloPopup.show<void>(
|
||||
context,
|
||||
title: 'verif lagi penuh',
|
||||
body: 'coba lagi nanti, atau lanjut tanpa verif aja.',
|
||||
icon: const Icon(
|
||||
Icons.lock_clock_rounded,
|
||||
size: 40,
|
||||
color: HaloTokens.brand,
|
||||
),
|
||||
primary: HaloPopupAction(
|
||||
label: 'lanjut tanpa verif',
|
||||
onPressed: () {},
|
||||
),
|
||||
secondary: HaloPopupAction(
|
||||
label: 'hubungi admin',
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
HaloButton(
|
||||
label: 'show snackbar',
|
||||
variant: HaloButtonVariant.secondary,
|
||||
onPressed: () => HaloSnackbar.show(
|
||||
context,
|
||||
'sisa 3 menit lagi ya',
|
||||
icon: '⏳',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_divider(),
|
||||
|
||||
_section('Input'),
|
||||
const TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'mau dipanggil apa?',
|
||||
labelText: 'nama panggilan',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: HaloSpacing.s48),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _section(String title) => Padding(
|
||||
padding: const EdgeInsets.only(top: HaloSpacing.s16, bottom: HaloSpacing.s8),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.brandDark,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _divider() => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: HaloSpacing.s16),
|
||||
child: Divider(),
|
||||
);
|
||||
}
|
||||
|
||||
class _Swatch extends StatelessWidget {
|
||||
const _Swatch(this.name, this.color, {this.label = HaloTokens.ink});
|
||||
final String name;
|
||||
final Color color;
|
||||
final Color label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 110,
|
||||
height: 64,
|
||||
padding: const EdgeInsets.all(HaloSpacing.s8),
|
||||
alignment: Alignment.bottomLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: HaloRadius.md,
|
||||
border: Border.all(color: HaloTokens.border),
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: label,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
316
client_app/lib/core/theme/halo_theme.dart
Normal file
316
client_app/lib/core/theme/halo_theme.dart
Normal file
@@ -0,0 +1,316 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'halo_tokens.dart';
|
||||
|
||||
ThemeData haloThemeData() {
|
||||
final base = ColorScheme.fromSeed(
|
||||
seedColor: HaloTokens.brand,
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
|
||||
final colorScheme = base.copyWith(
|
||||
primary: HaloTokens.brand,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: HaloTokens.brandSoft,
|
||||
onPrimaryContainer: HaloTokens.brandDark,
|
||||
secondary: HaloTokens.accent,
|
||||
onSecondary: HaloTokens.ink,
|
||||
secondaryContainer: HaloTokens.accentSoft,
|
||||
onSecondaryContainer: HaloTokens.brandDark,
|
||||
surface: HaloTokens.surface,
|
||||
onSurface: HaloTokens.ink,
|
||||
surfaceContainerHighest: HaloTokens.bg,
|
||||
error: HaloTokens.danger,
|
||||
onError: Colors.white,
|
||||
outline: HaloTokens.border,
|
||||
);
|
||||
|
||||
const textTheme = TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 36,
|
||||
height: 40 / 36,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 30,
|
||||
height: 34 / 30,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.4,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 26,
|
||||
height: 30 / 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.3,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 22,
|
||||
height: 28 / 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 22,
|
||||
height: 28 / 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 18,
|
||||
height: 24 / 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
height: 22 / 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 16,
|
||||
height: 24 / 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
height: 22 / 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 13,
|
||||
height: 18 / 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
height: 22 / 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 12,
|
||||
height: 16 / 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 10,
|
||||
height: 14 / 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
color: HaloTokens.inkMuted,
|
||||
),
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: HaloTokens.bg,
|
||||
textTheme: textTheme,
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: HaloTokens.bg,
|
||||
foregroundColor: HaloTokens.ink,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 22,
|
||||
height: 28 / 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: HaloTokens.brand,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: HaloTokens.brandSoft,
|
||||
disabledForegroundColor: HaloTokens.inkMuted,
|
||||
elevation: 0,
|
||||
shadowColor: const Color(0x59E17A9D),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s24,
|
||||
vertical: HaloSpacing.s16,
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: HaloRadius.pill),
|
||||
textStyle: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: HaloTokens.brandDark,
|
||||
side: const BorderSide(color: HaloTokens.border),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s24,
|
||||
vertical: HaloSpacing.s16,
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: HaloRadius.pill),
|
||||
textStyle: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: HaloTokens.brandDark,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s16,
|
||||
vertical: HaloSpacing.s12,
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: HaloRadius.pill),
|
||||
textStyle: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: HaloTokens.surface,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s20,
|
||||
vertical: HaloSpacing.s20,
|
||||
),
|
||||
constraints: BoxConstraints(minHeight: 64),
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
color: HaloTokens.inkMuted,
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 13,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: HaloRadius.lg,
|
||||
borderSide: BorderSide(color: HaloTokens.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: HaloRadius.lg,
|
||||
borderSide: BorderSide(color: HaloTokens.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: HaloRadius.lg,
|
||||
borderSide: BorderSide(color: HaloTokens.brand, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: HaloRadius.lg,
|
||||
borderSide: BorderSide(color: HaloTokens.danger),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: HaloRadius.lg,
|
||||
borderSide: BorderSide(color: HaloTokens.danger, width: 2),
|
||||
),
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
backgroundColor: HaloTokens.surface,
|
||||
surfaceTintColor: HaloTokens.surface,
|
||||
modalBackgroundColor: HaloTokens.surface,
|
||||
modalBarrierColor: Color(0x66000000),
|
||||
elevation: 0,
|
||||
modalElevation: 0,
|
||||
showDragHandle: true,
|
||||
dragHandleColor: HaloTokens.brandSoft,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
),
|
||||
dialogTheme: const DialogThemeData(
|
||||
backgroundColor: HaloTokens.surface,
|
||||
surfaceTintColor: HaloTokens.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: HaloRadius.xl),
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
contentTextStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
height: 22 / 15,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
),
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: HaloTokens.ink,
|
||||
contentTextStyle: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: HaloRadius.pill),
|
||||
elevation: 4,
|
||||
insetPadding: EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s16,
|
||||
vertical: HaloSpacing.s12,
|
||||
),
|
||||
actionTextColor: HaloTokens.brandSoft,
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: HaloTokens.surface,
|
||||
selectedColor: HaloTokens.brand,
|
||||
disabledColor: HaloTokens.brandSoft.withValues(alpha: 0.5),
|
||||
labelStyle: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
secondaryLabelStyle: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s16,
|
||||
vertical: HaloSpacing.s8,
|
||||
),
|
||||
side: const BorderSide(color: HaloTokens.border),
|
||||
shape: const RoundedRectangleBorder(borderRadius: HaloRadius.pill),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: HaloTokens.border,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
129
client_app/lib/core/theme/halo_tokens.dart
Normal file
129
client_app/lib/core/theme/halo_tokens.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Design tokens for the HaloBestie warm palette.
|
||||
///
|
||||
/// Mirrors `requirement/Figma/handoff/tokens.json`. Three palettes
|
||||
/// (warm/calm/playful) exist in the source-of-truth JSON; only `warm`
|
||||
/// ships in code today — the others are stubbed for phase 5.
|
||||
///
|
||||
/// Naming convention: every token prefixed with `Halo*` and grouped into
|
||||
/// purpose classes (`HaloTokens` for colors, `HaloSpacing`, `HaloRadius`,
|
||||
/// `HaloMotion`, `HaloShadows`).
|
||||
class HaloTokens {
|
||||
const HaloTokens._();
|
||||
|
||||
// Warm palette — default.
|
||||
static const Color bg = Color(0xFFFDF7F4);
|
||||
static const Color surface = Color(0xFFFFFFFF);
|
||||
static const Color ink = Color(0xFF2A1820);
|
||||
static const Color inkSoft = Color(0xFF6B5560);
|
||||
static const Color inkMuted = Color(0xFF9C8590);
|
||||
static const Color brand = Color(0xFFE17A9D);
|
||||
static const Color brandDark = Color(0xFF8C3255);
|
||||
static const Color brandSoft = Color(0xFFF7E4E9);
|
||||
static const Color brandSofter = Color(0xFFFBEFF3);
|
||||
static const Color accent = Color(0xFFF7B26A);
|
||||
static const Color accentSoft = Color(0xFFFCEAD3);
|
||||
static const Color mint = Color(0xFFB8DBC8);
|
||||
static const Color lilac = Color(0xFFD4C5E8);
|
||||
static const Color success = Color(0xFF5BA67F);
|
||||
static const Color danger = Color(0xFFD86B6B);
|
||||
static const Color border = Color(0xFFF0E4E8);
|
||||
|
||||
// Font family names — must match the `family:` entries in pubspec.yaml.
|
||||
// Falls back to system fonts when the .ttf assets are not bundled.
|
||||
static const String fontDisplay = 'BricolageGrotesque';
|
||||
static const String fontBody = 'Poppins';
|
||||
static const String fontMono = 'JetBrainsMono';
|
||||
|
||||
// TODO: phase5 — calm palette
|
||||
// static const Color calmBg = Color(0xFFF6F4F8);
|
||||
// static const Color calmBrand = Color(0xFF9B8BC4);
|
||||
// ...
|
||||
|
||||
// TODO: phase5 — playful palette
|
||||
// static const Color playfulBg = Color(0xFFFFF5F8);
|
||||
// static const Color playfulBrand = Color(0xFFFF69A0);
|
||||
// ...
|
||||
}
|
||||
|
||||
class HaloSpacing {
|
||||
const HaloSpacing._();
|
||||
|
||||
static const double s0 = 0;
|
||||
static const double s4 = 4;
|
||||
static const double s8 = 8;
|
||||
static const double s12 = 12;
|
||||
static const double s16 = 16;
|
||||
static const double s20 = 20;
|
||||
static const double s24 = 24;
|
||||
static const double s32 = 32;
|
||||
static const double s40 = 40;
|
||||
static const double s48 = 48;
|
||||
static const double s64 = 64;
|
||||
static const double s80 = 80;
|
||||
}
|
||||
|
||||
class HaloRadius {
|
||||
const HaloRadius._();
|
||||
|
||||
static const Radius _sm = Radius.circular(8);
|
||||
static const Radius _md = Radius.circular(12);
|
||||
static const Radius _lg = Radius.circular(16);
|
||||
static const Radius _xl = Radius.circular(22);
|
||||
static const Radius _pill = Radius.circular(9999);
|
||||
|
||||
static const BorderRadius sm = BorderRadius.all(_sm);
|
||||
static const BorderRadius md = BorderRadius.all(_md);
|
||||
static const BorderRadius lg = BorderRadius.all(_lg);
|
||||
static const BorderRadius xl = BorderRadius.all(_xl);
|
||||
static const BorderRadius pill = BorderRadius.all(_pill);
|
||||
}
|
||||
|
||||
class HaloMotion {
|
||||
const HaloMotion._();
|
||||
|
||||
static const Duration fast = Duration(milliseconds: 180);
|
||||
static const Duration normal = Duration(milliseconds: 280);
|
||||
static const Duration slow = Duration(milliseconds: 420);
|
||||
|
||||
static const Cubic ease = Cubic(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
class HaloShadows {
|
||||
const HaloShadows._();
|
||||
|
||||
static const List<BoxShadow> soft = [
|
||||
BoxShadow(
|
||||
color: Color(0x0A8C3255),
|
||||
offset: Offset(0, 1),
|
||||
blurRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0x0F8C3255),
|
||||
offset: Offset(0, 8),
|
||||
blurRadius: 24,
|
||||
),
|
||||
];
|
||||
|
||||
static const List<BoxShadow> card = [
|
||||
BoxShadow(
|
||||
color: Color(0x0D8C3255),
|
||||
offset: Offset(0, 2),
|
||||
blurRadius: 6,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0x1A8C3255),
|
||||
offset: Offset(0, 18),
|
||||
blurRadius: 40,
|
||||
),
|
||||
];
|
||||
|
||||
static const List<BoxShadow> button = [
|
||||
BoxShadow(
|
||||
color: Color(0x59E17A9D),
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 14,
|
||||
),
|
||||
];
|
||||
}
|
||||
42
client_app/lib/core/theme/widgets/halo_bottom_sheet.dart
Normal file
42
client_app/lib/core/theme/widgets/halo_bottom_sheet.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
class HaloBottomSheet {
|
||||
const HaloBottomSheet._();
|
||||
|
||||
static Future<T?> show<T>(
|
||||
BuildContext context, {
|
||||
required Widget child,
|
||||
bool isDismissible = true,
|
||||
bool enableDrag = true,
|
||||
bool isScrollControlled = false,
|
||||
}) {
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isDismissible: isDismissible,
|
||||
enableDrag: enableDrag,
|
||||
isScrollControlled: isScrollControlled,
|
||||
backgroundColor: HaloTokens.surface,
|
||||
barrierColor: const Color(0x66000000),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
showDragHandle: true,
|
||||
builder: (ctx) => SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
HaloSpacing.s24,
|
||||
HaloSpacing.s8,
|
||||
HaloSpacing.s24,
|
||||
HaloSpacing.s24,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
client_app/lib/core/theme/widgets/halo_button.dart
Normal file
152
client_app/lib/core/theme/widgets/halo_button.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
enum HaloButtonVariant { primary, secondary, ghost }
|
||||
|
||||
enum HaloButtonSize { sm, md, lg }
|
||||
|
||||
class HaloButton extends StatelessWidget {
|
||||
const HaloButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.variant = HaloButtonVariant.primary,
|
||||
this.size = HaloButtonSize.md,
|
||||
this.icon,
|
||||
this.fullWidth = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final HaloButtonVariant variant;
|
||||
final HaloButtonSize size;
|
||||
final Widget? icon;
|
||||
final bool fullWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final disabled = onPressed == null;
|
||||
final padding = _padding();
|
||||
final fontSize = _fontSize();
|
||||
const shape = RoundedRectangleBorder(borderRadius: HaloRadius.pill);
|
||||
final textStyle = TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
Widget child = _content(textStyle);
|
||||
|
||||
Widget button;
|
||||
switch (variant) {
|
||||
case HaloButtonVariant.primary:
|
||||
button = Container(
|
||||
decoration: disabled
|
||||
? null
|
||||
: const BoxDecoration(
|
||||
borderRadius: HaloRadius.pill,
|
||||
boxShadow: HaloShadows.button,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: HaloTokens.brand,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: HaloTokens.brandSoft,
|
||||
disabledForegroundColor: HaloTokens.inkMuted,
|
||||
elevation: 0,
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case HaloButtonVariant.secondary:
|
||||
button = OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: HaloTokens.brandDark,
|
||||
disabledForegroundColor: HaloTokens.inkMuted,
|
||||
backgroundColor: HaloTokens.surface,
|
||||
side: BorderSide(
|
||||
color: disabled ? HaloTokens.border : HaloTokens.brandSoft,
|
||||
),
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
break;
|
||||
case HaloButtonVariant.ghost:
|
||||
button = TextButton(
|
||||
onPressed: onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: HaloTokens.brandDark,
|
||||
disabledForegroundColor: HaloTokens.inkMuted,
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (fullWidth) {
|
||||
return SizedBox(width: double.infinity, child: button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
Widget _content(TextStyle textStyle) {
|
||||
if (icon == null) {
|
||||
return Text(label, style: textStyle);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconTheme(
|
||||
data: IconThemeData(size: textStyle.fontSize! + 2),
|
||||
child: icon!,
|
||||
),
|
||||
const SizedBox(width: HaloSpacing.s8),
|
||||
Text(label, style: textStyle),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsets _padding() {
|
||||
switch (size) {
|
||||
case HaloButtonSize.sm:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s16,
|
||||
vertical: HaloSpacing.s8,
|
||||
);
|
||||
case HaloButtonSize.md:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s24,
|
||||
vertical: HaloSpacing.s12,
|
||||
);
|
||||
case HaloButtonSize.lg:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s32,
|
||||
vertical: HaloSpacing.s16,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double _fontSize() {
|
||||
switch (size) {
|
||||
case HaloButtonSize.sm:
|
||||
return 13;
|
||||
case HaloButtonSize.md:
|
||||
return 15;
|
||||
case HaloButtonSize.lg:
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
client_app/lib/core/theme/widgets/halo_chip.dart
Normal file
75
client_app/lib/core/theme/widgets/halo_chip.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
class HaloChip extends StatelessWidget {
|
||||
const HaloChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback? onTap;
|
||||
final Widget? icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final disabled = onTap == null;
|
||||
final bgColor = selected
|
||||
? HaloTokens.brand
|
||||
: disabled
|
||||
? HaloTokens.brandSofter
|
||||
: HaloTokens.surface;
|
||||
final fgColor = selected
|
||||
? Colors.white
|
||||
: disabled
|
||||
? HaloTokens.inkMuted
|
||||
: HaloTokens.ink;
|
||||
final borderColor = selected ? HaloTokens.brand : HaloTokens.border;
|
||||
|
||||
return Material(
|
||||
color: bgColor,
|
||||
borderRadius: HaloRadius.pill,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: HaloRadius.pill,
|
||||
child: AnimatedContainer(
|
||||
duration: HaloMotion.fast,
|
||||
curve: HaloMotion.ease,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s16,
|
||||
vertical: HaloSpacing.s8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: borderColor),
|
||||
borderRadius: HaloRadius.pill,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
IconTheme(
|
||||
data: IconThemeData(size: 16, color: fgColor),
|
||||
child: icon!,
|
||||
),
|
||||
const SizedBox(width: HaloSpacing.s8),
|
||||
],
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: fgColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
client_app/lib/core/theme/widgets/halo_orb.dart
Normal file
61
client_app/lib/core/theme/widgets/halo_orb.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
/// A soft gradient circle used as an avatar/identity glyph.
|
||||
///
|
||||
/// `seed` deterministically picks a hue blend from the warm palette.
|
||||
class HaloOrb extends StatelessWidget {
|
||||
const HaloOrb({
|
||||
super.key,
|
||||
required this.seed,
|
||||
this.size = 64,
|
||||
this.label,
|
||||
});
|
||||
|
||||
final int seed;
|
||||
final double size;
|
||||
final String? label;
|
||||
|
||||
static const List<List<Color>> _gradients = [
|
||||
[HaloTokens.brand, HaloTokens.brandDark],
|
||||
[HaloTokens.accent, HaloTokens.brand],
|
||||
[HaloTokens.lilac, HaloTokens.brand],
|
||||
[HaloTokens.mint, HaloTokens.accent],
|
||||
[HaloTokens.brandSoft, HaloTokens.brand],
|
||||
[HaloTokens.accentSoft, HaloTokens.accent],
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = _gradients[seed.abs() % _gradients.length];
|
||||
final initial = (label ?? '').isNotEmpty
|
||||
? label!.substring(0, 1).toUpperCase()
|
||||
: null;
|
||||
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: colors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: HaloShadows.soft,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: initial == null
|
||||
? null
|
||||
: Text(
|
||||
initial,
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: size * 0.42,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
client_app/lib/core/theme/widgets/halo_popup.dart
Normal file
94
client_app/lib/core/theme/widgets/halo_popup.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
import 'halo_button.dart';
|
||||
|
||||
class HaloPopupAction {
|
||||
const HaloPopupAction({required this.label, required this.onPressed});
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
}
|
||||
|
||||
class HaloPopup {
|
||||
const HaloPopup._();
|
||||
|
||||
static Future<T?> show<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
String? body,
|
||||
Widget? icon,
|
||||
HaloPopupAction? primary,
|
||||
HaloPopupAction? secondary,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierColor: const Color(0x66000000),
|
||||
builder: (ctx) => Dialog(
|
||||
backgroundColor: HaloTokens.surface,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: HaloRadius.xl),
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: HaloSpacing.s24),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(HaloSpacing.s24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Center(child: icon),
|
||||
const SizedBox(height: HaloSpacing.s16),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontDisplay,
|
||||
fontSize: 22,
|
||||
height: 28 / 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: HaloTokens.ink,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (body != null) ...[
|
||||
const SizedBox(height: HaloSpacing.s12),
|
||||
Text(
|
||||
body,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 15,
|
||||
height: 22 / 15,
|
||||
color: HaloTokens.inkSoft,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: HaloSpacing.s24),
|
||||
if (primary != null)
|
||||
HaloButton(
|
||||
label: primary.label,
|
||||
fullWidth: true,
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
primary.onPressed();
|
||||
},
|
||||
),
|
||||
if (secondary != null) ...[
|
||||
const SizedBox(height: HaloSpacing.s8),
|
||||
HaloButton(
|
||||
label: secondary.label,
|
||||
variant: HaloButtonVariant.ghost,
|
||||
fullWidth: true,
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
secondary.onPressed();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
client_app/lib/core/theme/widgets/halo_snackbar.dart
Normal file
58
client_app/lib/core/theme/widgets/halo_snackbar.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
class HaloSnackbar {
|
||||
const HaloSnackbar._();
|
||||
|
||||
static void show(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? icon,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
if (messenger == null) return;
|
||||
messenger
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
duration: duration,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: HaloTokens.ink,
|
||||
elevation: 4,
|
||||
shape: const RoundedRectangleBorder(borderRadius: HaloRadius.pill),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s16,
|
||||
vertical: HaloSpacing.s12,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: HaloSpacing.s20,
|
||||
vertical: HaloSpacing.s12,
|
||||
),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Text(
|
||||
icon,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(width: HaloSpacing.s8),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
client_app/lib/core/theme/widgets/halo_step_dots.dart
Normal file
43
client_app/lib/core/theme/widgets/halo_step_dots.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../halo_tokens.dart';
|
||||
|
||||
class HaloStepDots extends StatelessWidget {
|
||||
const HaloStepDots({
|
||||
super.key,
|
||||
required this.total,
|
||||
required this.current,
|
||||
}) : assert(total > 0),
|
||||
assert(current >= 1);
|
||||
|
||||
final int total;
|
||||
final int current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(total, (index) {
|
||||
final step = index + 1;
|
||||
final active = step == current;
|
||||
final past = step < current;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: index == total - 1 ? 0 : HaloSpacing.s8),
|
||||
child: AnimatedContainer(
|
||||
duration: HaloMotion.fast,
|
||||
curve: HaloMotion.ease,
|
||||
width: active ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? HaloTokens.brand
|
||||
: past
|
||||
? HaloTokens.brandSoft
|
||||
: HaloTokens.border,
|
||||
borderRadius: HaloRadius.pill,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
client_app/lib/core/theme/widgets/widgets.dart
Normal file
7
client_app/lib/core/theme/widgets/widgets.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
export 'halo_bottom_sheet.dart';
|
||||
export 'halo_button.dart';
|
||||
export 'halo_chip.dart';
|
||||
export 'halo_orb.dart';
|
||||
export 'halo_popup.dart';
|
||||
export 'halo_snackbar.dart';
|
||||
export 'halo_step_dots.dart';
|
||||
Reference in New Issue
Block a user