diet-guard/docs/flutter-rules-ui.md
Krzysztof kuhy Rudnicki 17a2c62f26 Automate CLAUDE.md's structural rules, then cut it to 189 lines
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
2026-08-15 16:39:57 +02:00

6.4 KiB

UI: theming, layout and overlays

Vendored Flutter/Dart AI rules. See CLAUDE.md.

UI Theming and Styling Code

  • Responsiveness: Use LayoutBuilder or MediaQuery to create responsive UIs.
  • Text: Use Theme.of(context).textTheme for text styles.
  • Text Fields: Configure textCapitalization, keyboardType, and placeholder.

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 theme and darkTheme to your MaterialApp to support system brightness settings seamlessly.
  • Centralize Component Styles: Customize specific component themes (e.g., elevatedButtonTheme, cardTheme, appBarTheme) within ThemeData to ensure consistency.
  • Dark/Light Mode and Theme Toggle: Implement support for both light and dark themes using theme and darkTheme properties of MaterialApp. The themeMode property can be dynamically controlled (e.g., via a ChangeNotifierProvider) to allow for toggling between ThemeMode.light, ThemeMode.dark, or ThemeMode.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 copyWith and lerp: These methods are required for the extension to work correctly with theme transitions.
  • Register in ThemeData: Add your custom extension to the extensions list in your ThemeData.
  • 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 a Set<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 combine Flexible and Expanded in the same Row or Column.
  • Wrap: Use when you have a series of widgets that would overflow a Row or Column, 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 a Stack by anchoring it to the edges.
  • Align: Use to position a child within a Stack using alignments like Alignment.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 the OverlayEntry for 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'),
          ),
        );
      }
    }