mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 16:33:56 +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
202 lines
6.4 KiB
Markdown
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'),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|