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

202 lines
6.4 KiB
Markdown

<!--
Vendored from flutter/flutter docs/rules/rules.md, split across five files
so each stays under this repo's 250-line cap. Upstream content: edit it here
only to re-sync with upstream, not to record decisions about this repo --
those belong in CLAUDE.md.
-->
# UI: theming, layout and overlays
_Vendored Flutter/Dart AI rules. See [CLAUDE.md](../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`.
```dart
// 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.
```dart
// 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.
```dart
// 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.
```dart
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'),
),
);
}
}
```