mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
CLAUDE.md was 1101 lines and loaded into every session in this repo. Three
passes, in order: automate what a gate can check, rewrite what is left as
instructions rather than rationale, extract what still does not fit.
Automated. Five prohibitions were enforced by nothing but a paragraph asking a
future reader not to do something; they are now repo_invariants_test.dart:
- no app/linux/ (a GTK embedder target manages ~20fps at 4K)
- dart:io only in *_io.dart seams and lib/desktop/ -- this one is why the
test exists at all, since dart:io does not fail a web *compile*. It throws
at runtime, so a bad import is a blank white window that the widget tests
happily pass through.
- no meal builder / repeat-last-meal / reward prompt
- no image_picker, no CAMERA permission
- wrapper port 8732 and the diet-guard-desktop Chrome profile, both pinned
because IndexedDB is keyed by origin and moving either hides the whole
local food log
Each guard was canary-tested by breaking the invariant and confirming the
suite goes red -- a guard that cannot fail is decoration.
Rules whose behaviour was already covered (budget epoch seeding, sync merge,
food bank) shrink to a pointer at the test that owns them.
Where a rule's entire value was its rationale ("the removal is the feature"),
the why moved into the name and reason string of the gate that now enforces
it, so it lives somewhere executable instead of somewhere quotable.
Extracted. The vendored flutter/flutter rules were 789 of the 1101 lines --
72% of the file, none of it about this repo. Split into five docs/ topic files
under the cap and linked. Verified content-complete: 664 non-blank lines in,
664 out, zero missing.
Python 692 tests / 100% coverage unchanged; Flutter 548 -> 555 (+7 new).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK3HCZrM75sRHL1gL8usve
6.4 KiB
6.4 KiB
UI: theming, layout and overlays
Vendored Flutter/Dart AI rules. See CLAUDE.md.
UI Theming and Styling Code
- Responsiveness: Use
LayoutBuilderorMediaQueryto create responsive UIs. - Text: Use
Theme.of(context).textThemefor text styles. - Text Fields: Configure
textCapitalization,keyboardType, andplaceholder.
Material Theming Best Practices
Embrace ThemeData and Material 3
- Use
ColorScheme.fromSeed(): Use this to generate a complete, harmonious color palette for both light and dark modes from a single seed color. - Define Light and Dark Themes: Provide both
themeanddarkThemeto yourMaterialAppto support system brightness settings seamlessly. - Centralize Component Styles: Customize specific component themes (e.g.,
elevatedButtonTheme,cardTheme,appBarTheme) withinThemeDatato ensure consistency. - Dark/Light Mode and Theme Toggle: Implement support for both light and
dark themes using
themeanddarkThemeproperties ofMaterialApp. ThethemeModeproperty can be dynamically controlled (e.g., via aChangeNotifierProvider) to allow for toggling betweenThemeMode.light,ThemeMode.dark, orThemeMode.system.
// main.dart
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
),
textTheme: const TextTheme(
displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold),
bodyMedium: TextStyle(fontSize: 14.0, height: 1.4),
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
),
home: const MyHomePage(),
);
Implement Design Tokens with ThemeExtension
For custom styles that aren't part of the standard ThemeData, use
ThemeExtension to define reusable design tokens.
- Create a Custom Theme Extension: Define a class that extends
ThemeExtension<T>and include your custom properties. - Implement
copyWithandlerp: These methods are required for the extension to work correctly with theme transitions. - Register in
ThemeData: Add your custom extension to theextensionslist in yourThemeData. - Access Tokens in Widgets: Use
Theme.of(context).extension<MyColors>()!to access your custom tokens.
// 1. Define the extension
@immutable
class MyColors extends ThemeExtension<MyColors> {
const MyColors({required this.success, required this.danger});
final Color? success;
final Color? danger;
@override
ThemeExtension<MyColors> copyWith({Color? success, Color? danger}) {
return MyColors(success: success ?? this.success, danger: danger ?? this.danger);
}
@override
ThemeExtension<MyColors> lerp(ThemeExtension<MyColors>? other, double t) {
if (other is! MyColors) return this;
return MyColors(
success: Color.lerp(success, other.success, t),
danger: Color.lerp(danger, other.danger, t),
);
}
}
// 2. Register it in ThemeData
theme: ThemeData(
extensions: const <ThemeExtension<dynamic>>[
MyColors(success: Colors.green, danger: Colors.red),
],
),
// 3. Use it in a widget
Container(
color: Theme.of(context).extension<MyColors>()!.success,
)
Styling with WidgetStateProperty
WidgetStateProperty.resolveWith: Provide a function that receives aSet<WidgetState>and returns the appropriate value for the current state.WidgetStateProperty.all: A shorthand for when the value is the same for all states.
// Example: Creating a button style that changes color when pressed.
final ButtonStyle myButtonStyle = ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return Colors.green; // Color when pressed
}
return Colors.red; // Default color
},
),
);
Layout Best Practices
Building Flexible and Overflow-Safe Layouts
For Rows and Columns
Expanded: Use to make a child widget fill the remaining available space along the main axis.Flexible: Use when you want a widget to shrink to fit, but not necessarily grow. Don't combineFlexibleandExpandedin the sameRoworColumn.Wrap: Use when you have a series of widgets that would overflow aRoworColumn, and you want them to move to the next line.
For General Content
SingleChildScrollView: Use when your content is intrinsically larger than the viewport, but is a fixed size.ListView/GridView: For long lists or grids of content, always use a builder constructor (.builder).FittedBox: Use to scale or fit a single child widget within its parent.LayoutBuilder: Use for complex, responsive layouts to make decisions based on the available space.
Layering Widgets with Stack
Positioned: Use to precisely place a child within aStackby anchoring it to the edges.Align: Use to position a child within aStackusing alignments likeAlignment.center.
Advanced Layout with Overlays
-
OverlayPortal: Use this widget to show UI elements (like custom dropdowns or tooltips) "on top" of everything else. It manages theOverlayEntryfor you.class MyDropdown extends StatefulWidget { const MyDropdown({super.key}); @override State<MyDropdown> createState() => _MyDropdownState(); } class _MyDropdownState extends State<MyDropdown> { final _controller = OverlayPortalController(); @override Widget build(BuildContext context) { return OverlayPortal( controller: _controller, overlayChildBuilder: (BuildContext context) { return const Positioned( top: 50, left: 10, child: Card( child: Padding( padding: EdgeInsets.all(8.0), child: Text('I am an overlay!'), ), ), ); }, child: ElevatedButton( onPressed: _controller.toggle, child: const Text('Toggle Overlay'), ), ); } }