brutalist_ui 0.0.1
brutalist_ui: ^0.0.1 copied to clipboard
A neobrutalist design library for Flutter, built entirely on Flutter's core widget primitives.
import 'package:brutalist_ui/brutalist_ui.dart';
void main() {
runApp(const GalleryApp());
}
class GalleryApp extends StatefulWidget {
const GalleryApp({super.key});
@override
State<GalleryApp> createState() => _GalleryAppState();
}
class _GalleryAppState extends State<GalleryApp> {
bool _dark = false;
@override
Widget build(BuildContext context) {
return NeoApp(
title: 'brutalist_ui',
theme: _dark ? const NeoThemeData.dark() : const NeoThemeData(),
home: GalleryScreen(
dark: _dark,
onDarkChanged: (bool value) => setState(() => _dark = value),
),
);
}
}
class GalleryScreen extends StatefulWidget {
const GalleryScreen({
super.key,
required this.dark,
required this.onDarkChanged,
});
final bool dark;
final ValueChanged<bool> onDarkChanged;
@override
State<GalleryScreen> createState() => _GalleryScreenState();
}
const List<NeoMenuEntry<String>> _fruitEntries = <NeoMenuEntry<String>>[
NeoMenuEntry<String>(value: 'apple', label: 'Apple'),
NeoMenuEntry<String>(value: 'banana', label: 'Banana'),
NeoMenuEntry<String>(value: 'cherry', label: 'Cherry'),
NeoMenuEntry<String>(
value: 'durian',
label: 'Durian (sold out)',
enabled: false,
),
];
class _GalleryScreenState extends State<GalleryScreen> {
final TextEditingController _nameController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final Set<String> _tags = <String>{'flutter'};
bool _subscribed = true;
bool _notifications = false;
double _volume = 0.4;
String _plan = 'monthly';
String? _fruit;
int _navIndex = 0;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
void _toggleTag(String tag, bool selected) {
setState(() {
if (selected) {
_tags.add(tag);
} else {
_tags.remove(tag);
}
});
}
Future<void> _confirmDelete() {
return showNeoDialog<void>(
context: context,
builder: (BuildContext context) {
return NeoDialog(
title: const Text('DELETE PROJECT?'),
content: const Text(
'This cannot be undone. Everything in it goes away.',
),
actions: <Widget>[
NeoButton(
variant: NeoVariant.neutral,
size: NeoButtonSize.small,
onPressed: () => Navigator.of(context).pop(),
child: const Text('CANCEL'),
),
NeoButton(
variant: NeoVariant.danger,
size: NeoButtonSize.small,
onPressed: () => Navigator.of(context).pop(),
child: const Text('DELETE'),
),
],
);
},
);
}
Future<void> _showOptionsSheet() {
return showNeoSheet<void>(
context: context,
builder: (BuildContext context) {
return NeoBottomSheet(
title: const Text('OPTIONS'),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
NeoListTile(title: const Text('Duplicate'), onPressed: () {}),
const SizedBox(height: 10),
NeoListTile(title: const Text('Archive'), onPressed: () {}),
const SizedBox(height: 18),
NeoButton(
variant: NeoVariant.neutral,
onPressed: () => Navigator.of(context).pop(),
child: const Text('CLOSE'),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final NeoThemeData theme = NeoTheme.of(context);
return NeoScaffold(
bottomBar: NeoBottomNavBar(
currentIndex: _navIndex,
onSelected: (int index) => setState(() => _navIndex = index),
items: const <NeoNavItem>[
NeoNavItem(icon: NeoIcon(NeoGlyph.check), label: 'Browse'),
NeoNavItem(icon: NeoIcon(NeoGlyph.plus), label: 'Create'),
NeoNavItem(icon: NeoIcon(NeoGlyph.chevronRight), label: 'Profile'),
],
),
appBar: NeoAppBar(
title: const Text('NEOBRUTALIST'),
actions: <Widget>[
NeoSwitch(
value: widget.dark,
variant: NeoVariant.secondary,
semanticLabel: 'Dark mode',
onChanged: widget.onDarkChanged,
),
],
),
floatingAction: NeoIconButton(
variant: NeoVariant.danger,
size: NeoButtonSize.large,
semanticLabel: 'Delete project',
onPressed: _confirmDelete,
icon: const NeoIcon(NeoGlyph.close),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 110),
children: <Widget>[
Text('Built on widgets.dart', style: theme.typography.display),
const SizedBox(height: 8),
Text(
'No Material. No Cupertino.',
style: theme.typography.body.copyWith(
color: theme.colors.mutedForeground,
),
),
const _Section(
title: 'BUTTONS',
child: Wrap(
spacing: 12,
runSpacing: 12,
children: <Widget>[
_DemoButton(variant: NeoVariant.primary, label: 'PRIMARY'),
_DemoButton(variant: NeoVariant.secondary, label: 'SECONDARY'),
_DemoButton(variant: NeoVariant.accent, label: 'ACCENT'),
_DemoButton(variant: NeoVariant.success, label: 'SUCCESS'),
_DemoButton(variant: NeoVariant.warning, label: 'WARNING'),
_DemoButton(variant: NeoVariant.danger, label: 'DANGER'),
_DemoButton(variant: NeoVariant.neutral, label: 'NEUTRAL'),
NeoButton(onPressed: null, child: Text('DISABLED')),
],
),
),
_Section(
title: 'SIZES',
child: Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
NeoButton(
size: NeoButtonSize.small,
onPressed: () {},
child: const Text('SMALL'),
),
NeoButton(onPressed: () {}, child: const Text('MEDIUM')),
NeoButton(
size: NeoButtonSize.large,
onPressed: () {},
child: const Text('LARGE'),
),
NeoIconButton(
onPressed: () {},
icon: const NeoIcon(NeoGlyph.plus),
),
NeoIconButton(
variant: NeoVariant.primary,
onPressed: () {},
icon: const NeoIcon(NeoGlyph.chevronRight),
),
],
),
),
_Section(
title: 'TEXT INPUT',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
NeoTextField(
controller: _nameController,
placeholder: 'Your name',
semanticLabel: 'Your name',
),
const SizedBox(height: 12),
const NeoTextField(placeholder: 'Disabled', enabled: false),
],
),
),
_Section(
title: 'SELECTION',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
NeoCheckbox(
value: _subscribed,
semanticLabel: 'Subscribe',
onChanged: (bool value) =>
setState(() => _subscribed = value),
),
const SizedBox(width: 12),
const Text('Subscribe to the newsletter'),
],
),
const SizedBox(height: 16),
Row(
children: <Widget>[
NeoSwitch(
value: _notifications,
semanticLabel: 'Notifications',
onChanged: (bool value) =>
setState(() => _notifications = value),
),
const SizedBox(width: 12),
const Text('Push notifications'),
],
),
const SizedBox(height: 16),
RadioGroup<String>(
groupValue: _plan,
onChanged: (String? value) => setState(() => _plan = value!),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_PlanOption(value: 'monthly', label: 'Monthly'),
SizedBox(height: 12),
_PlanOption(value: 'yearly', label: 'Yearly'),
],
),
),
],
),
),
_Section(
title: 'SLIDER & PROGRESS',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
NeoSlider(
value: _volume,
semanticLabel: 'Volume',
onChanged: (double value) => setState(() => _volume = value),
),
const SizedBox(height: 20),
NeoProgressBar(value: _volume, semanticLabel: 'Volume'),
const SizedBox(height: 10),
Text(
'${(_volume * 100).round()}%',
style: theme.typography.label,
),
],
),
),
_Section(
title: 'CHIPS & BADGES',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Wrap(
spacing: 10,
runSpacing: 10,
children: <Widget>[
for (final String tag in <String>[
'flutter',
'dart',
'design',
'brutal',
])
NeoChip(
label: Text(tag.toUpperCase()),
selected: _tags.contains(tag),
onSelected: (bool selected) =>
_toggleTag(tag, selected),
),
],
),
const SizedBox(height: 16),
const Wrap(
spacing: 10,
runSpacing: 10,
children: <Widget>[
NeoBadge(child: Text('NEW')),
NeoBadge(
variant: NeoVariant.success,
child: Text('SHIPPED'),
),
NeoBadge(variant: NeoVariant.danger, child: Text('BROKEN')),
],
),
],
),
),
_Section(
title: 'DROPDOWN',
child: NeoDropdown<String>(
value: _fruit,
entries: _fruitEntries,
placeholder: 'Pick a fruit',
semanticLabel: 'Fruit',
onChanged: (String value) => setState(() => _fruit = value),
),
),
_Section(
title: 'FORM VALIDATION',
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
NeoTextFormField(
placeholder: 'you@example.com',
semanticLabel: 'Email',
validator: (String? value) => (value ?? '').contains('@')
? null
: 'Enter a valid email',
),
const SizedBox(height: 14),
NeoButton(
variant: NeoVariant.success,
onPressed: () => _formKey.currentState?.validate(),
child: const Text('SUBMIT'),
),
],
),
),
),
_Section(
title: 'OVERLAYS & LOADING',
child: Wrap(
spacing: 16,
runSpacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
NeoButton(
variant: NeoVariant.secondary,
onPressed: _showOptionsSheet,
child: const Text('BOTTOM SHEET'),
),
NeoTooltip(
message: 'This is a tooltip',
child: NeoButton(
variant: NeoVariant.neutral,
onPressed: () {},
child: const Text('HOVER ME'),
),
),
const NeoActivityIndicator(),
const NeoActivityIndicator(
size: 24,
variant: NeoVariant.danger,
),
],
),
),
_Section(
title: 'SURFACES',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
NeoCard(
color: theme.colors.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('A flat card', style: theme.typography.title),
const SizedBox(height: 6),
const Text('Hard border, hard shadow, no gradient.'),
],
),
),
const NeoDivider(),
NeoListTile(
leading: const NeoIcon(NeoGlyph.check),
title: const Text('Tappable tile'),
subtitle: const Text('It presses into the page'),
trailing: const NeoIcon(NeoGlyph.chevronRight),
onPressed: () {},
),
],
),
),
],
),
);
}
}
class _Section extends StatelessWidget {
const _Section({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
final NeoThemeData theme = NeoTheme.of(context);
return Padding(
padding: const EdgeInsets.only(top: 36),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
title,
style: theme.typography.label.copyWith(
color: theme.colors.mutedForeground,
),
),
const SizedBox(height: 14),
child,
],
),
);
}
}
class _DemoButton extends StatelessWidget {
const _DemoButton({required this.variant, required this.label});
final NeoVariant variant;
final String label;
@override
Widget build(BuildContext context) {
return NeoButton(variant: variant, onPressed: () {}, child: Text(label));
}
}
class _PlanOption extends StatelessWidget {
const _PlanOption({required this.value, required this.label});
final String value;
final String label;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
NeoRadio<String>(value: value),
const SizedBox(width: 12),
Text(label),
],
);
}
}