From b907b67e1dfec7b4fe9cf601c23908130449414c Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 4 Mar 2026 15:29:16 -0300 Subject: [PATCH 01/12] feat: adding very good ui skill --- .claude-plugin/plugin.json | 1 + CLAUDE.md | 1 + README.md | 2 + skills/very-good-ui/SKILL.md | 448 +++++++++++++++++++++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 skills/very-good-ui/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a033baf..3f283b6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -46,6 +46,7 @@ "testing", "theming", "very-good-cli", + "very-good-ui", "wcag", "widget-testing", "sdk-upgrade", diff --git a/CLAUDE.md b/CLAUDE.md index 714dde0..60955a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,7 @@ skills/ static-security/reference.md testing/SKILL.md testing/reference.md + very-good-ui/SKILL.md ``` ## Skill File Format diff --git a/README.md b/README.md index f7e68b8..6e73069 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ For more details, see the [Very Good Claude Marketplace][marketplace_link]. | [**Bloc**](skills/bloc/SKILL.md) | State management with Bloc/Cubit — sealed events & states, `BlocProvider`/`BlocBuilder` widgets, event transformers, and testing with `blocTest()` & `mocktail` | | [**Layered Architecture**](skills/layered-architecture/SKILL.md) | VGV layered architecture — four-layer package structure (Data, Repository, Business Logic, Presentation), dependency rules, data flow, and bootstrap wiring | | [**Security**](skills/static-security/SKILL.md) | Flutter-specific static security review — secrets management, `flutter_secure_storage`, certificate pinning, `Random.secure()`, `formz` validation, dependency vulnerability scanning with `osv-scanner`, and OWASP Mobile Top 10 guidance | +| [**Very Good UI**](skills/very-good-ui/SKILL.md) | Flutter UI package creation — custom widget libraries with theming via `InheritedWidget`, design tokens, barrel file exports, golden tests, widget tests, gallery apps, and consistent API conventions | | [**License Compliance**](skills/license-compliance/SKILL.md) | Dependency license auditing — categorizes licenses (permissive, weak/strong copyleft, unknown), flags non-compliant or missing licenses, and produces a structured compliance report using Very Good CLI | | [**Dart/Flutter SDK Upgrade**](skills/dart-flutter-sdk-upgrade/SKILL.md) | Bump Dart and Flutter SDK constraints across packages — CI workflow versions, pubspec.yaml environment constraints, and PR preparation for SDK upgrades | @@ -76,6 +77,7 @@ You can also invoke skills directly as slash commands: /vgv-navigation /vgv-static-security /vgv-testing +/vgv-very-good-ui /vgv-license-compliance /vgv-dart-flutter-sdk-upgrade ``` diff --git a/skills/very-good-ui/SKILL.md b/skills/very-good-ui/SKILL.md new file mode 100644 index 0000000..bd79877 --- /dev/null +++ b/skills/very-good-ui/SKILL.md @@ -0,0 +1,448 @@ +--- +name: very-good-ui +description: Best practices for building a Flutter UI package on top of Material — custom components, ThemeExtension-based theming, consistent APIs, and widget tests. +--- + +# Very Good UI + +Best practices for creating a Flutter UI package — a reusable widget library that builds on top of `package:flutter/material.dart`, extending it with app-specific components, custom design tokens via `ThemeExtension`, and a consistent API surface. + +## Core Standards + +Apply these standards to ALL UI package work: + +- **Build on Material** — depend on `flutter/material.dart` and compose Material widgets; do not rebuild primitives that Material already provides +- **One widget per file** — each public widget lives in its own file named after the widget in snake_case (e.g., `app_button.dart`) +- **Barrel file for public API** — expose all public widgets and theme classes through a single barrel file (e.g., `lib/my_ui.dart`) that also re-exports `material.dart` +- **Extend theming with `ThemeExtension`** — use Material's `ThemeData`, `ColorScheme`, and `TextTheme` as the base; add app-specific tokens (spacing, custom colors) via `ThemeExtension` +- **Every widget has a corresponding widget test** — behavioral tests verify interactions, callbacks, and state changes +- **Prefix all public classes** — use a consistent prefix (e.g., `App`, `Vg`) to avoid naming collisions with Material widgets +- **Use `const` constructors everywhere possible** — all widget constructors must be `const` when feasible +- **Document every public member** — every public class, constructor parameter, and method has a dartdoc comment + +## Package Structure + +``` +my_ui/ +├── lib/ +│ ├── my_ui.dart # Barrel file — re-exports material.dart + all public API +│ └── src/ +│ ├── theme/ +│ │ ├── app_theme.dart # AppTheme class with light/dark ThemeData builders +│ │ ├── app_colors.dart # AppColors ThemeExtension for custom color tokens +│ │ ├── app_spacing.dart # AppSpacing ThemeExtension for spacing tokens +│ │ └── app_text_styles.dart # Optional: extra text styles beyond Material's TextTheme +│ ├── widgets/ +│ │ ├── app_button.dart +│ │ ├── app_text_field.dart +│ │ ├── app_card.dart +│ │ └── ... +│ └── extensions/ +│ └── build_context_extensions.dart # context.appColors, context.appSpacing shortcuts +├── test/ +│ ├── src/ +│ │ ├── theme/ +│ │ │ └── app_theme_test.dart +│ │ └── widgets/ +│ │ ├── app_button_test.dart +│ │ └── ... +│ └── helpers/ +│ └── pump_app.dart # Test helper wrapping widgets in MaterialApp + theme +├── gallery/ # Optional: standalone app showcasing widgets +│ └── ... +└── pubspec.yaml +``` + +## Theming System + +### Custom Color Tokens via ThemeExtension + +Use `ThemeExtension` for colors that go beyond Material's `ColorScheme`: + +```dart +class AppColors extends ThemeExtension { + const AppColors({ + required this.success, + required this.onSuccess, + required this.warning, + required this.onWarning, + required this.info, + required this.onInfo, + }); + + final Color success; + final Color onSuccess; + final Color warning; + final Color onWarning; + final Color info; + final Color onInfo; + + @override + AppColors copyWith({ + Color? success, + Color? onSuccess, + Color? warning, + Color? onWarning, + Color? info, + Color? onInfo, + }) { + return AppColors( + success: success ?? this.success, + onSuccess: onSuccess ?? this.onSuccess, + warning: warning ?? this.warning, + onWarning: onWarning ?? this.onWarning, + info: info ?? this.info, + onInfo: onInfo ?? this.onInfo, + ); + } + + @override + AppColors lerp(AppColors? other, double t) { + if (other is! AppColors) return this; + return AppColors( + success: Color.lerp(success, other.success, t)!, + onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!, + warning: Color.lerp(warning, other.warning, t)!, + onWarning: Color.lerp(onWarning, other.onWarning, t)!, + info: Color.lerp(info, other.info, t)!, + onInfo: Color.lerp(onInfo, other.onInfo, t)!, + ); + } +} +``` + +### Spacing Tokens via ThemeExtension + +```dart +class AppSpacing extends ThemeExtension { + const AppSpacing({ + this.xxs = 4, + this.xs = 8, + this.sm = 12, + this.md = 16, + this.lg = 24, + this.xlg = 32, + this.xxlg = 48, + }); + + final double xxs; + final double xs; + final double sm; + final double md; + final double lg; + final double xlg; + final double xxlg; + + @override + AppSpacing copyWith({ + double? xxs, + double? xs, + double? sm, + double? md, + double? lg, + double? xlg, + double? xxlg, + }) { + return AppSpacing( + xxs: xxs ?? this.xxs, + xs: xs ?? this.xs, + sm: sm ?? this.sm, + md: md ?? this.md, + lg: lg ?? this.lg, + xlg: xlg ?? this.xlg, + xxlg: xxlg ?? this.xxlg, + ); + } + + @override + AppSpacing lerp(AppSpacing? other, double t) { + if (other is! AppSpacing) return this; + return AppSpacing( + xxs: lerpDouble(xxs, other.xxs, t)!, + xs: lerpDouble(xs, other.xs, t)!, + sm: lerpDouble(sm, other.sm, t)!, + md: lerpDouble(md, other.md, t)!, + lg: lerpDouble(lg, other.lg, t)!, + xlg: lerpDouble(xlg, other.xlg, t)!, + xxlg: lerpDouble(xxlg, other.xxlg, t)!, + ); + } +} +``` + +### BuildContext Extensions + +Provide shorthand access for custom tokens: + +```dart +extension AppThemeBuildContext on BuildContext { + AppColors get appColors => + Theme.of(this).extension()!; + + AppSpacing get appSpacing => + Theme.of(this).extension()!; +} +``` + +### AppTheme Class + +Compose Material's `ThemeData` with custom extensions: + +```dart +class AppTheme { + static ThemeData get light { + const appColors = AppColors( + success: Color(0xFF16A34A), + onSuccess: Color(0xFFFFFFFF), + warning: Color(0xFFCA8A04), + onWarning: Color(0xFFFFFFFF), + info: Color(0xFF2563EB), + onInfo: Color(0xFFFFFFFF), + ); + + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF4F46E5), + ), + extensions: const [ + appColors, + AppSpacing(), + ], + ); + } + + static ThemeData get dark { + const appColors = AppColors( + success: Color(0xFF4ADE80), + onSuccess: Color(0xFF1C1B1F), + warning: Color(0xFFFACC15), + onWarning: Color(0xFF1C1B1F), + info: Color(0xFF60A5FA), + onInfo: Color(0xFF1C1B1F), + ); + + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF4F46E5), + brightness: Brightness.dark, + ), + extensions: const [ + appColors, + AppSpacing(), + ], + ); + } +} +``` + +## Building Widgets + +### Widget API Guidelines + +- Compose Material widgets — use `FilledButton`, `OutlinedButton`, `TextField`, `Card`, etc. as building blocks +- Accept only the minimum required parameters — avoid "kitchen sink" constructors +- Use named parameters for everything except `key` and `child`/`children` +- Provide sensible defaults derived from the theme when a parameter is not supplied +- Expose callbacks with `ValueChanged` or `VoidCallback` — do not use raw `Function` +- Use `Widget?` for optional slot-based composition (leading, trailing icons, etc.) + +### Example Widget + +```dart +/// A styled button from the UI package. +/// +/// Wraps Material's [FilledButton] and [OutlinedButton] with app-specific +/// sizing and theming. +class AppButton extends StatelessWidget { + /// Creates an [AppButton]. + const AppButton({ + required this.onPressed, + required this.child, + this.variant = AppButtonVariant.primary, + this.size = AppButtonSize.medium, + super.key, + }); + + /// Called when the button is tapped. + final VoidCallback? onPressed; + + /// The button's content, typically a [Text] widget. + final Widget child; + + /// The visual variant of the button. + final AppButtonVariant variant; + + /// The size of the button. + final AppButtonSize size; + + @override + Widget build(BuildContext context) { + final spacing = context.appSpacing; + + final padding = switch (size) { + AppButtonSize.small => EdgeInsets.symmetric( + horizontal: spacing.sm, + vertical: spacing.xxs, + ), + AppButtonSize.medium => EdgeInsets.symmetric( + horizontal: spacing.md, + vertical: spacing.xs, + ), + AppButtonSize.large => EdgeInsets.symmetric( + horizontal: spacing.lg, + vertical: spacing.sm, + ), + }; + + final style = ButtonStyle( + padding: WidgetStatePropertyAll(padding), + ); + + return switch (variant) { + AppButtonVariant.primary => FilledButton( + onPressed: onPressed, + style: style, + child: child, + ), + AppButtonVariant.secondary => FilledButton.tonal( + onPressed: onPressed, + style: style, + child: child, + ), + AppButtonVariant.outline => OutlinedButton( + onPressed: onPressed, + style: style, + child: child, + ), + }; + } +} + +/// Visual variants for [AppButton]. +enum AppButtonVariant { primary, secondary, outline } + +/// Size variants for [AppButton]. +enum AppButtonSize { small, medium, large } +``` + +## Testing + +### Test Helper + +Create a `pumpApp` helper that wraps widgets in a `MaterialApp` with the full theme: + +```dart +extension PumpApp on WidgetTester { + Future pumpApp( + Widget widget, { + ThemeData? theme, + }) { + return pumpWidget( + MaterialApp( + theme: theme ?? AppTheme.light, + home: Scaffold(body: widget), + ), + ); + } +} +``` + +### Widget Test + +```dart +void main() { + group('AppButton', () { + testWidgets('renders child', (tester) async { + await tester.pumpApp( + AppButton( + onPressed: () {}, + child: const Text('Tap me'), + ), + ); + + expect(find.text('Tap me'), findsOneWidget); + }); + + testWidgets('calls onPressed when tapped', (tester) async { + var tapped = false; + await tester.pumpApp( + AppButton( + onPressed: () => tapped = true, + child: const Text('Tap me'), + ), + ); + + await tester.tap(find.byType(AppButton)); + expect(tapped, isTrue); + }); + + testWidgets('does not call onPressed when disabled', (tester) async { + var tapped = false; + await tester.pumpApp( + AppButton( + onPressed: null, + child: const Text('Disabled'), + ), + ); + + await tester.tap(find.byType(AppButton)); + expect(tapped, isFalse); + }); + }); +} +``` + +## Barrel File + +Re-export Material and the full public API through a single barrel file: + +```dart +/// My UI — a custom Flutter widget library built on Material. +library; + +export 'package:flutter/material.dart'; + +export 'src/extensions/build_context_extensions.dart'; +export 'src/theme/app_colors.dart'; +export 'src/theme/app_spacing.dart'; +export 'src/theme/app_theme.dart'; +export 'src/widgets/app_button.dart'; +export 'src/widgets/app_card.dart'; +export 'src/widgets/app_text_field.dart'; +``` + +## Anti-Patterns + +| Anti-Pattern | Correct Approach | +| ------------ | ---------------- | +| Rebuilding widgets Material already provides (e.g., custom button from `GestureDetector` + `DecoratedBox`) | Compose Material widgets (`FilledButton`, `OutlinedButton`) and style them | +| Creating a parallel theme system with custom `InheritedWidget` | Use Material's `ThemeData` as the base and `ThemeExtension` for custom tokens | +| Hardcoding `Color(0xFF...)` in widget code | Use `Theme.of(context).colorScheme` for standard colors and `context.appColors` for custom tokens | +| Duplicating Material's `ColorScheme` roles in a custom class | Only create `ThemeExtension` tokens for values Material doesn't cover (e.g., success, warning, info) | +| Using `dynamic` or `Object` for callback types | Use `VoidCallback`, `ValueChanged`, or specific function typedefs | +| Exposing internal implementation files directly | Use a barrel file; keep all files under `src/` private | + +## Common Workflows + +### Adding a New Widget + +1. Create `lib/src/widgets/app_.dart` with a `const` constructor and dartdoc +2. Compose Material widgets internally; read custom tokens via `context.appColors` / `context.appSpacing` +3. Export the file from the barrel file (`lib/my_ui.dart`) +4. Create `test/src/widgets/app__test.dart` with widget tests + +### Adding a New Custom Token + +1. Add the token to the appropriate `ThemeExtension` class (`AppColors` or `AppSpacing`) +2. Update `copyWith` and `lerp` methods +3. Update `AppTheme.light` and `AppTheme.dark` to include the new token value +4. Update existing tests that construct the extension directly +5. Use the new token in widgets via the `BuildContext` extension + +### Creating the Package + +Use Very Good CLI to scaffold the package: + +```bash +very_good create flutter_package my_ui --description "A custom Flutter UI package" +``` From 32260ab7412ee2c783b99ebea481c4f0fb9030d2 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 4 Mar 2026 15:54:12 -0300 Subject: [PATCH 02/12] adding widgetbook part --- skills/very-good-ui/SKILL.md | 220 ++++++++++++++++++++++++++++++++++- 1 file changed, 219 insertions(+), 1 deletion(-) diff --git a/skills/very-good-ui/SKILL.md b/skills/very-good-ui/SKILL.md index bd79877..45e53f7 100644 --- a/skills/very-good-ui/SKILL.md +++ b/skills/very-good-ui/SKILL.md @@ -48,7 +48,7 @@ my_ui/ │ │ └── ... │ └── helpers/ │ └── pump_app.dart # Test helper wrapping widgets in MaterialApp + theme -├── gallery/ # Optional: standalone app showcasing widgets +├── widgetbook/ # Widgetbook catalog submodule (sandbox + showcase) │ └── ... └── pubspec.yaml ``` @@ -411,6 +411,222 @@ export 'src/widgets/app_card.dart'; export 'src/widgets/app_text_field.dart'; ``` +## Widgetbook Catalog + +The UI package includes a `widgetbook/` submodule — a standalone Flutter app powered by [Widgetbook](https://pub.dev/packages/widgetbook) that serves as both a **developer sandbox** for building widgets in isolation and a **showcase** for browsing every widget in the package. + +### Catalog Structure + +``` +my_ui/ +├── lib/ +│ └── ... # UI package source +├── widgetbook/ # Catalog submodule +│ ├── lib/ +│ │ ├── main.dart # Entry point — runs WidgetbookApp +│ │ └── widgetbook/ +│ │ ├── widgetbook.dart # WidgetbookApp widget with addons +│ │ ├── widgetbook.directories.g.dart # Generated — do not edit +│ │ ├── use_cases/ +│ │ │ ├── app_button.dart # Use cases for AppButton +│ │ │ ├── app_card.dart +│ │ │ └── ... # One file per widget +│ │ └── widgets/ +│ │ ├── widgets.dart # Barrel file for catalog helpers +│ │ └── use_case_decorator.dart # Wrapper for consistent presentation +│ ├── pubspec.yaml # Package name: widgetbook_catalog +│ ├── analysis_options.yaml +│ └── .gitignore +└── pubspec.yaml +``` + +### pubspec.yaml + +The catalog depends on the UI package via a path reference and uses Widgetbook's annotation + code generation approach: + +```yaml +name: widgetbook_catalog +description: "Widgetbook catalog for My UI" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +dependencies: + flutter: + sdk: flutter + my_ui: + path: .. + widgetbook: ^3.7.0 + widgetbook_annotation: ^3.1.0 + +dev_dependencies: + build_runner: ^2.4.7 + flutter_test: + sdk: flutter + very_good_analysis: ^7.0.0 + widgetbook_generator: ^3.7.0 + +flutter: + uses-material-design: true +``` + +### Entry Point + +```dart +import 'package:flutter/material.dart'; +import 'package:widgetbook_catalog/widgetbook/widgetbook.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const WidgetbookApp()); +} +``` + +### WidgetbookApp + +The root widget configures Widgetbook with theme addons and a use-case decorator: + +```dart +import 'package:flutter/material.dart'; +import 'package:my_ui/my_ui.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; +import 'package:widgetbook_catalog/widgetbook/widgetbook.directories.g.dart'; +import 'package:widgetbook_catalog/widgetbook/widgets/widgets.dart'; + +@widgetbook.App() +class WidgetbookApp extends StatelessWidget { + const WidgetbookApp({super.key}); + + @override + Widget build(BuildContext context) { + return Widgetbook.material( + directories: directories, + addons: [ + BuilderAddon( + name: 'Decorator', + builder: (context, child) { + return UseCaseDecorator(child: child); + }, + ), + ThemeAddon( + themes: [ + WidgetbookTheme(name: 'Light', data: AppTheme.light), + WidgetbookTheme(name: 'Dark', data: AppTheme.dark), + ], + themeBuilder: (context, theme, child) { + return Theme( + data: theme, + child: DefaultTextStyle( + style: theme.textTheme.bodyMedium ?? const TextStyle(), + child: child, + ), + ); + }, + ), + ], + ); + } +} +``` + +### Use-Case Decorator + +A wrapper widget that provides a consistent background for all use cases: + +```dart +class UseCaseDecorator extends StatelessWidget { + const UseCaseDecorator({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: SizedBox.expand(child: Material(child: child)), + ); + } +} +``` + +### Writing Use Cases + +Each widget gets a dedicated file in `use_cases/` with one or more `@widgetbook.UseCase` annotations. Each use case is a top-level function that returns a `Widget`: + +```dart +import 'package:flutter/material.dart'; +import 'package:my_ui/my_ui.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase(name: 'primary', type: AppButton) +Widget primary(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + child: const Text('Primary'), + ), + ); + +@widgetbook.UseCase(name: 'secondary', type: AppButton) +Widget secondary(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + variant: AppButtonVariant.secondary, + child: const Text('Secondary'), + ), + ); + +@widgetbook.UseCase(name: 'outline', type: AppButton) +Widget outline(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + variant: AppButtonVariant.outline, + child: const Text('Outline'), + ), + ); + +@widgetbook.UseCase(name: 'disabled', type: AppButton) +Widget disabled(BuildContext context) => const Center( + child: AppButton( + onPressed: null, + child: Text('Disabled'), + ), + ); + +@widgetbook.UseCase(name: 'all sizes', type: AppButton) +Widget allSizes(BuildContext context) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + for (final size in AppButtonSize.values) + AppButton( + onPressed: () {}, + size: size, + child: Text(size.name), + ), + ], + ), + ); +``` + +### Code Generation + +Widgetbook uses `build_runner` to scan `@widgetbook.UseCase` annotations and generate the `widgetbook.directories.g.dart` file. Run the generator after adding or modifying use cases: + +```bash +cd widgetbook && dart run build_runner build --delete-conflicting-outputs +``` + +### Running the Catalog + +```bash +cd widgetbook && flutter run -d chrome +``` + ## Anti-Patterns | Anti-Pattern | Correct Approach | @@ -430,6 +646,8 @@ export 'src/widgets/app_text_field.dart'; 2. Compose Material widgets internally; read custom tokens via `context.appColors` / `context.appSpacing` 3. Export the file from the barrel file (`lib/my_ui.dart`) 4. Create `test/src/widgets/app__test.dart` with widget tests +5. Add use cases in `widgetbook/lib/widgetbook/use_cases/app_.dart` covering all variants +6. Re-run `dart run build_runner build --delete-conflicting-outputs` in `widgetbook/` ### Adding a New Custom Token From b94411cf3c5215acc389c4d94836f0f50271a08e Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 4 Mar 2026 15:56:00 -0300 Subject: [PATCH 03/12] adding some missing words to cspell --- config/cspell.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/cspell.json b/config/cspell.json index eba95a4..8c58b5d 100644 --- a/config/cspell.json +++ b/config/cspell.json @@ -4,6 +4,7 @@ "Bidirectionality", "Bienvenido", "bypassable", + "dartdoc", "CSPRNG", "dismissable", "elemento", @@ -14,6 +15,7 @@ "GHSA", "goldens", "hoverable", + "lerp", "jailbroken", "LTRB", "mapbox", @@ -26,6 +28,8 @@ "serialization", "stdio", "WCAG", + "widgetbook", + "Widgetbook", "xxlg" ], "flagWords": [] From 40dfe7cfb18a65355f71c3d2f7603a5e55f972cc Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 4 Mar 2026 17:40:30 -0300 Subject: [PATCH 04/12] some suggestions --- CLAUDE.md | 1 + skills/very-good-ui/SKILL.md | 549 +++---------------------------- skills/very-good-ui/reference.md | 494 +++++++++++++++++++++++++++ 3 files changed, 541 insertions(+), 503 deletions(-) create mode 100644 skills/very-good-ui/reference.md diff --git a/CLAUDE.md b/CLAUDE.md index 60955a0..ee6425a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,7 @@ skills/ testing/SKILL.md testing/reference.md very-good-ui/SKILL.md + very-good-ui/reference.md ``` ## Skill File Format diff --git a/skills/very-good-ui/SKILL.md b/skills/very-good-ui/SKILL.md index 45e53f7..b75caa2 100644 --- a/skills/very-good-ui/SKILL.md +++ b/skills/very-good-ui/SKILL.md @@ -55,185 +55,18 @@ my_ui/ ## Theming System -### Custom Color Tokens via ThemeExtension +Use Material's `ThemeData`, `ColorScheme`, and `TextTheme` as the base. Add app-specific tokens via `ThemeExtension` — only for values Material does not cover (e.g., success, warning, info colors; spacing scale). -Use `ThemeExtension` for colors that go beyond Material's `ColorScheme`: +### Key Classes -```dart -class AppColors extends ThemeExtension { - const AppColors({ - required this.success, - required this.onSuccess, - required this.warning, - required this.onWarning, - required this.info, - required this.onInfo, - }); - - final Color success; - final Color onSuccess; - final Color warning; - final Color onWarning; - final Color info; - final Color onInfo; - - @override - AppColors copyWith({ - Color? success, - Color? onSuccess, - Color? warning, - Color? onWarning, - Color? info, - Color? onInfo, - }) { - return AppColors( - success: success ?? this.success, - onSuccess: onSuccess ?? this.onSuccess, - warning: warning ?? this.warning, - onWarning: onWarning ?? this.onWarning, - info: info ?? this.info, - onInfo: onInfo ?? this.onInfo, - ); - } - - @override - AppColors lerp(AppColors? other, double t) { - if (other is! AppColors) return this; - return AppColors( - success: Color.lerp(success, other.success, t)!, - onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!, - warning: Color.lerp(warning, other.warning, t)!, - onWarning: Color.lerp(onWarning, other.onWarning, t)!, - info: Color.lerp(info, other.info, t)!, - onInfo: Color.lerp(onInfo, other.onInfo, t)!, - ); - } -} -``` - -### Spacing Tokens via ThemeExtension - -```dart -class AppSpacing extends ThemeExtension { - const AppSpacing({ - this.xxs = 4, - this.xs = 8, - this.sm = 12, - this.md = 16, - this.lg = 24, - this.xlg = 32, - this.xxlg = 48, - }); - - final double xxs; - final double xs; - final double sm; - final double md; - final double lg; - final double xlg; - final double xxlg; - - @override - AppSpacing copyWith({ - double? xxs, - double? xs, - double? sm, - double? md, - double? lg, - double? xlg, - double? xxlg, - }) { - return AppSpacing( - xxs: xxs ?? this.xxs, - xs: xs ?? this.xs, - sm: sm ?? this.sm, - md: md ?? this.md, - lg: lg ?? this.lg, - xlg: xlg ?? this.xlg, - xxlg: xxlg ?? this.xxlg, - ); - } +| Class | Purpose | +| ----- | ------- | +| `AppColors extends ThemeExtension` | Custom color tokens beyond `ColorScheme` (success, warning, info + on-variants) | +| `AppSpacing extends ThemeExtension` | Spacing scale (xxs through xxlg) with `copyWith` and `lerp` | +| `AppTheme` | Composes `ThemeData` with `ColorScheme.fromSeed`, custom extensions, for light and dark variants | +| `AppThemeBuildContext` extension | Shorthand `context.appColors` and `context.appSpacing` | - @override - AppSpacing lerp(AppSpacing? other, double t) { - if (other is! AppSpacing) return this; - return AppSpacing( - xxs: lerpDouble(xxs, other.xxs, t)!, - xs: lerpDouble(xs, other.xs, t)!, - sm: lerpDouble(sm, other.sm, t)!, - md: lerpDouble(md, other.md, t)!, - lg: lerpDouble(lg, other.lg, t)!, - xlg: lerpDouble(xlg, other.xlg, t)!, - xxlg: lerpDouble(xxlg, other.xxlg, t)!, - ); - } -} -``` - -### BuildContext Extensions - -Provide shorthand access for custom tokens: - -```dart -extension AppThemeBuildContext on BuildContext { - AppColors get appColors => - Theme.of(this).extension()!; - - AppSpacing get appSpacing => - Theme.of(this).extension()!; -} -``` - -### AppTheme Class - -Compose Material's `ThemeData` with custom extensions: - -```dart -class AppTheme { - static ThemeData get light { - const appColors = AppColors( - success: Color(0xFF16A34A), - onSuccess: Color(0xFFFFFFFF), - warning: Color(0xFFCA8A04), - onWarning: Color(0xFFFFFFFF), - info: Color(0xFF2563EB), - onInfo: Color(0xFFFFFFFF), - ); - - return ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF4F46E5), - ), - extensions: const [ - appColors, - AppSpacing(), - ], - ); - } - - static ThemeData get dark { - const appColors = AppColors( - success: Color(0xFF4ADE80), - onSuccess: Color(0xFF1C1B1F), - warning: Color(0xFFFACC15), - onWarning: Color(0xFF1C1B1F), - info: Color(0xFF60A5FA), - onInfo: Color(0xFF1C1B1F), - ); - - return ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF4F46E5), - brightness: Brightness.dark, - ), - extensions: const [ - appColors, - AppSpacing(), - ], - ); - } -} -``` +Every `ThemeExtension` must implement `copyWith` and `lerp` for theme animation support. ## Building Widgets @@ -246,85 +79,6 @@ class AppTheme { - Expose callbacks with `ValueChanged` or `VoidCallback` — do not use raw `Function` - Use `Widget?` for optional slot-based composition (leading, trailing icons, etc.) -### Example Widget - -```dart -/// A styled button from the UI package. -/// -/// Wraps Material's [FilledButton] and [OutlinedButton] with app-specific -/// sizing and theming. -class AppButton extends StatelessWidget { - /// Creates an [AppButton]. - const AppButton({ - required this.onPressed, - required this.child, - this.variant = AppButtonVariant.primary, - this.size = AppButtonSize.medium, - super.key, - }); - - /// Called when the button is tapped. - final VoidCallback? onPressed; - - /// The button's content, typically a [Text] widget. - final Widget child; - - /// The visual variant of the button. - final AppButtonVariant variant; - - /// The size of the button. - final AppButtonSize size; - - @override - Widget build(BuildContext context) { - final spacing = context.appSpacing; - - final padding = switch (size) { - AppButtonSize.small => EdgeInsets.symmetric( - horizontal: spacing.sm, - vertical: spacing.xxs, - ), - AppButtonSize.medium => EdgeInsets.symmetric( - horizontal: spacing.md, - vertical: spacing.xs, - ), - AppButtonSize.large => EdgeInsets.symmetric( - horizontal: spacing.lg, - vertical: spacing.sm, - ), - }; - - final style = ButtonStyle( - padding: WidgetStatePropertyAll(padding), - ); - - return switch (variant) { - AppButtonVariant.primary => FilledButton( - onPressed: onPressed, - style: style, - child: child, - ), - AppButtonVariant.secondary => FilledButton.tonal( - onPressed: onPressed, - style: style, - child: child, - ), - AppButtonVariant.outline => OutlinedButton( - onPressed: onPressed, - style: style, - child: child, - ), - }; - } -} - -/// Visual variants for [AppButton]. -enum AppButtonVariant { primary, secondary, outline } - -/// Size variants for [AppButton]. -enum AppButtonSize { small, medium, large } -``` - ## Testing ### Test Helper @@ -347,50 +101,12 @@ extension PumpApp on WidgetTester { } ``` -### Widget Test +### Test Patterns -```dart -void main() { - group('AppButton', () { - testWidgets('renders child', (tester) async { - await tester.pumpApp( - AppButton( - onPressed: () {}, - child: const Text('Tap me'), - ), - ); - - expect(find.text('Tap me'), findsOneWidget); - }); - - testWidgets('calls onPressed when tapped', (tester) async { - var tapped = false; - await tester.pumpApp( - AppButton( - onPressed: () => tapped = true, - child: const Text('Tap me'), - ), - ); - - await tester.tap(find.byType(AppButton)); - expect(tapped, isTrue); - }); - - testWidgets('does not call onPressed when disabled', (tester) async { - var tapped = false; - await tester.pumpApp( - AppButton( - onPressed: null, - child: const Text('Disabled'), - ), - ); - - await tester.tap(find.byType(AppButton)); - expect(tapped, isFalse); - }); - }); -} -``` +- Test rendering: verify the widget shows the expected content +- Test interactions: verify callbacks fire on tap/input +- Test disabled state: verify callbacks do not fire when `onPressed` is `null` +- Test all variants: cover each enum value (variant, size, etc.) ## Barrel File @@ -413,219 +129,42 @@ export 'src/widgets/app_text_field.dart'; ## Widgetbook Catalog -The UI package includes a `widgetbook/` submodule — a standalone Flutter app powered by [Widgetbook](https://pub.dev/packages/widgetbook) that serves as both a **developer sandbox** for building widgets in isolation and a **showcase** for browsing every widget in the package. +The UI package includes a `widgetbook/` submodule — a standalone Flutter app powered by Widgetbook that serves as both a **developer sandbox** for building widgets in isolation and a **showcase** for browsing every widget in the package. The submodule package name in `pubspec.yaml` is `widgetbook_catalog`. ### Catalog Structure ``` -my_ui/ +widgetbook/ ├── lib/ -│ └── ... # UI package source -├── widgetbook/ # Catalog submodule -│ ├── lib/ -│ │ ├── main.dart # Entry point — runs WidgetbookApp -│ │ └── widgetbook/ -│ │ ├── widgetbook.dart # WidgetbookApp widget with addons -│ │ ├── widgetbook.directories.g.dart # Generated — do not edit -│ │ ├── use_cases/ -│ │ │ ├── app_button.dart # Use cases for AppButton -│ │ │ ├── app_card.dart -│ │ │ └── ... # One file per widget -│ │ └── widgets/ -│ │ ├── widgets.dart # Barrel file for catalog helpers -│ │ └── use_case_decorator.dart # Wrapper for consistent presentation -│ ├── pubspec.yaml # Package name: widgetbook_catalog -│ ├── analysis_options.yaml -│ └── .gitignore -└── pubspec.yaml -``` - -### pubspec.yaml - -The catalog depends on the UI package via a path reference and uses Widgetbook's annotation + code generation approach: - -```yaml -name: widgetbook_catalog -description: "Widgetbook catalog for My UI" -publish_to: 'none' -version: 1.0.0+1 - -environment: - sdk: '>=3.2.3 <4.0.0' - -dependencies: - flutter: - sdk: flutter - my_ui: - path: .. - widgetbook: ^3.7.0 - widgetbook_annotation: ^3.1.0 - -dev_dependencies: - build_runner: ^2.4.7 - flutter_test: - sdk: flutter - very_good_analysis: ^7.0.0 - widgetbook_generator: ^3.7.0 - -flutter: - uses-material-design: true -``` - -### Entry Point - -```dart -import 'package:flutter/material.dart'; -import 'package:widgetbook_catalog/widgetbook/widgetbook.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - runApp(const WidgetbookApp()); -} -``` - -### WidgetbookApp - -The root widget configures Widgetbook with theme addons and a use-case decorator: - -```dart -import 'package:flutter/material.dart'; -import 'package:my_ui/my_ui.dart'; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; -import 'package:widgetbook_catalog/widgetbook/widgetbook.directories.g.dart'; -import 'package:widgetbook_catalog/widgetbook/widgets/widgets.dart'; - -@widgetbook.App() -class WidgetbookApp extends StatelessWidget { - const WidgetbookApp({super.key}); - - @override - Widget build(BuildContext context) { - return Widgetbook.material( - directories: directories, - addons: [ - BuilderAddon( - name: 'Decorator', - builder: (context, child) { - return UseCaseDecorator(child: child); - }, - ), - ThemeAddon( - themes: [ - WidgetbookTheme(name: 'Light', data: AppTheme.light), - WidgetbookTheme(name: 'Dark', data: AppTheme.dark), - ], - themeBuilder: (context, theme, child) { - return Theme( - data: theme, - child: DefaultTextStyle( - style: theme.textTheme.bodyMedium ?? const TextStyle(), - child: child, - ), - ); - }, - ), - ], - ); - } -} -``` - -### Use-Case Decorator - -A wrapper widget that provides a consistent background for all use cases: - -```dart -class UseCaseDecorator extends StatelessWidget { - const UseCaseDecorator({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return ColoredBox( - color: colorScheme.surfaceContainerHighest, - child: SizedBox.expand(child: Material(child: child)), - ); - } -} -``` - -### Writing Use Cases - -Each widget gets a dedicated file in `use_cases/` with one or more `@widgetbook.UseCase` annotations. Each use case is a top-level function that returns a `Widget`: - -```dart -import 'package:flutter/material.dart'; -import 'package:my_ui/my_ui.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; - -@widgetbook.UseCase(name: 'primary', type: AppButton) -Widget primary(BuildContext context) => Center( - child: AppButton( - onPressed: () {}, - child: const Text('Primary'), - ), - ); - -@widgetbook.UseCase(name: 'secondary', type: AppButton) -Widget secondary(BuildContext context) => Center( - child: AppButton( - onPressed: () {}, - variant: AppButtonVariant.secondary, - child: const Text('Secondary'), - ), - ); - -@widgetbook.UseCase(name: 'outline', type: AppButton) -Widget outline(BuildContext context) => Center( - child: AppButton( - onPressed: () {}, - variant: AppButtonVariant.outline, - child: const Text('Outline'), - ), - ); - -@widgetbook.UseCase(name: 'disabled', type: AppButton) -Widget disabled(BuildContext context) => const Center( - child: AppButton( - onPressed: null, - child: Text('Disabled'), - ), - ); - -@widgetbook.UseCase(name: 'all sizes', type: AppButton) -Widget allSizes(BuildContext context) => Center( - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - for (final size in AppButtonSize.values) - AppButton( - onPressed: () {}, - size: size, - child: Text(size.name), - ), - ], - ), - ); +│ ├── main.dart # Entry point — runs WidgetbookApp +│ └── widgetbook/ +│ ├── widgetbook.dart # WidgetbookApp widget with addons +│ ├── widgetbook.directories.g.dart # Generated — do not edit +│ ├── use_cases/ +│ │ ├── app_button.dart # Use cases for AppButton +│ │ ├── app_card.dart +│ │ └── ... # One file per widget +│ └── widgets/ +│ ├── widgets.dart # Barrel file for catalog helpers +│ └── use_case_decorator.dart # Wrapper for consistent presentation +├── pubspec.yaml # Package name: widgetbook_catalog +├── analysis_options.yaml +└── .gitignore ``` -### Code Generation +### Key Concepts -Widgetbook uses `build_runner` to scan `@widgetbook.UseCase` annotations and generate the `widgetbook.directories.g.dart` file. Run the generator after adding or modifying use cases: +- **Use cases**: top-level functions annotated with `@widgetbook.UseCase(name:, type:)`, one file per widget in `use_cases/` +- **Use-case decorator**: a `UseCaseDecorator` widget that wraps every use case with a consistent background +- **Theme addon**: `ThemeAddon` wired to `AppTheme.light` and `AppTheme.dark` for switching themes in the catalog +- **Code generation**: Widgetbook uses `build_runner` to scan annotations and generate `widgetbook.directories.g.dart` -```bash -cd widgetbook && dart run build_runner build --delete-conflicting-outputs -``` +### Commands -### Running the Catalog - -```bash -cd widgetbook && flutter run -d chrome -``` +| Command | Purpose | +| ------- | ------- | +| `cd widgetbook && dart run build_runner build --delete-conflicting-outputs` | Regenerate use-case directories after adding/modifying use cases | +| `cd widgetbook && flutter run -d chrome` | Run the catalog locally | ## Anti-Patterns @@ -634,7 +173,7 @@ cd widgetbook && flutter run -d chrome | Rebuilding widgets Material already provides (e.g., custom button from `GestureDetector` + `DecoratedBox`) | Compose Material widgets (`FilledButton`, `OutlinedButton`) and style them | | Creating a parallel theme system with custom `InheritedWidget` | Use Material's `ThemeData` as the base and `ThemeExtension` for custom tokens | | Hardcoding `Color(0xFF...)` in widget code | Use `Theme.of(context).colorScheme` for standard colors and `context.appColors` for custom tokens | -| Duplicating Material's `ColorScheme` roles in a custom class | Only create `ThemeExtension` tokens for values Material doesn't cover (e.g., success, warning, info) | +| Duplicating Material's `ColorScheme` roles in a custom class | Only create `ThemeExtension` tokens for values Material does not cover (e.g., success, warning, info) | | Using `dynamic` or `Object` for callback types | Use `VoidCallback`, `ValueChanged`, or specific function typedefs | | Exposing internal implementation files directly | Use a barrel file; keep all files under `src/` private | @@ -642,7 +181,7 @@ cd widgetbook && flutter run -d chrome ### Adding a New Widget -1. Create `lib/src/widgets/app_.dart` with a `const` constructor and dartdoc +1. Create `lib/src/widgets/app_.dart` with a `const` constructor and documentation 2. Compose Material widgets internally; read custom tokens via `context.appColors` / `context.appSpacing` 3. Export the file from the barrel file (`lib/my_ui.dart`) 4. Create `test/src/widgets/app__test.dart` with widget tests @@ -664,3 +203,7 @@ Use Very Good CLI to scaffold the package: ```bash very_good create flutter_package my_ui --description "A custom Flutter UI package" ``` + +## Additional Resources + +See [reference.md](reference.md) for complete code examples: `ThemeExtension` implementations (`AppColors`, `AppSpacing`), `AppTheme` class with light/dark variants, `BuildContext` extensions, example widget (`AppButton`), widget tests, Widgetbook catalog setup (`pubspec.yaml`, `WidgetbookApp`, `UseCaseDecorator`, use-case files). diff --git a/skills/very-good-ui/reference.md b/skills/very-good-ui/reference.md new file mode 100644 index 0000000..3105cf0 --- /dev/null +++ b/skills/very-good-ui/reference.md @@ -0,0 +1,494 @@ +# Very Good UI — Reference + +Complete code examples for the Very Good UI skill: theming, widgets, testing, and Widgetbook catalog setup. + +--- + +## ThemeExtension: AppColors + +Custom color tokens for values beyond Material's `ColorScheme`: + +```dart +class AppColors extends ThemeExtension { + const AppColors({ + required this.success, + required this.onSuccess, + required this.warning, + required this.onWarning, + required this.info, + required this.onInfo, + }); + + final Color success; + final Color onSuccess; + final Color warning; + final Color onWarning; + final Color info; + final Color onInfo; + + @override + AppColors copyWith({ + Color? success, + Color? onSuccess, + Color? warning, + Color? onWarning, + Color? info, + Color? onInfo, + }) { + return AppColors( + success: success ?? this.success, + onSuccess: onSuccess ?? this.onSuccess, + warning: warning ?? this.warning, + onWarning: onWarning ?? this.onWarning, + info: info ?? this.info, + onInfo: onInfo ?? this.onInfo, + ); + } + + @override + AppColors lerp(AppColors? other, double t) { + if (other is! AppColors) return this; + return AppColors( + success: Color.lerp(success, other.success, t)!, + onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!, + warning: Color.lerp(warning, other.warning, t)!, + onWarning: Color.lerp(onWarning, other.onWarning, t)!, + info: Color.lerp(info, other.info, t)!, + onInfo: Color.lerp(onInfo, other.onInfo, t)!, + ); + } +} +``` + +--- + +## ThemeExtension: AppSpacing + +Spacing scale with consistent token names: + +```dart +class AppSpacing extends ThemeExtension { + const AppSpacing({ + this.xxs = 4, + this.xs = 8, + this.sm = 12, + this.md = 16, + this.lg = 24, + this.xlg = 32, + this.xxlg = 48, + }); + + final double xxs; + final double xs; + final double sm; + final double md; + final double lg; + final double xlg; + final double xxlg; + + @override + AppSpacing copyWith({ + double? xxs, + double? xs, + double? sm, + double? md, + double? lg, + double? xlg, + double? xxlg, + }) { + return AppSpacing( + xxs: xxs ?? this.xxs, + xs: xs ?? this.xs, + sm: sm ?? this.sm, + md: md ?? this.md, + lg: lg ?? this.lg, + xlg: xlg ?? this.xlg, + xxlg: xxlg ?? this.xxlg, + ); + } + + @override + AppSpacing lerp(AppSpacing? other, double t) { + if (other is! AppSpacing) return this; + return AppSpacing( + xxs: lerpDouble(xxs, other.xxs, t)!, + xs: lerpDouble(xs, other.xs, t)!, + sm: lerpDouble(sm, other.sm, t)!, + md: lerpDouble(md, other.md, t)!, + lg: lerpDouble(lg, other.lg, t)!, + xlg: lerpDouble(xlg, other.xlg, t)!, + xxlg: lerpDouble(xxlg, other.xxlg, t)!, + ); + } +} +``` + +--- + +## BuildContext Extensions + +Shorthand access for custom tokens: + +```dart +extension AppThemeBuildContext on BuildContext { + AppColors get appColors => + Theme.of(this).extension()!; + + AppSpacing get appSpacing => + Theme.of(this).extension()!; +} +``` + +--- + +## AppTheme Class + +Composes Material's `ThemeData` with custom extensions for light and dark variants: + +```dart +class AppTheme { + static ThemeData get light { + const appColors = AppColors( + success: Color(0xFF16A34A), + onSuccess: Color(0xFFFFFFFF), + warning: Color(0xFFCA8A04), + onWarning: Color(0xFFFFFFFF), + info: Color(0xFF2563EB), + onInfo: Color(0xFFFFFFFF), + ); + + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF4F46E5), + ), + extensions: const [ + appColors, + AppSpacing(), + ], + ); + } + + static ThemeData get dark { + const appColors = AppColors( + success: Color(0xFF4ADE80), + onSuccess: Color(0xFF1C1B1F), + warning: Color(0xFFFACC15), + onWarning: Color(0xFF1C1B1F), + info: Color(0xFF60A5FA), + onInfo: Color(0xFF1C1B1F), + ); + + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF4F46E5), + brightness: Brightness.dark, + ), + extensions: const [ + appColors, + AppSpacing(), + ], + ); + } +} +``` + +--- + +## Example Widget: AppButton + +A styled button composing Material's `FilledButton` and `OutlinedButton`: + +```dart +/// A styled button from the UI package. +/// +/// Wraps Material's [FilledButton] and [OutlinedButton] with app-specific +/// sizing and theming. +class AppButton extends StatelessWidget { + /// Creates an [AppButton]. + const AppButton({ + required this.onPressed, + required this.child, + this.variant = AppButtonVariant.primary, + this.size = AppButtonSize.medium, + super.key, + }); + + /// Called when the button is tapped. + final VoidCallback? onPressed; + + /// The button's content, typically a [Text] widget. + final Widget child; + + /// The visual variant of the button. + final AppButtonVariant variant; + + /// The size of the button. + final AppButtonSize size; + + @override + Widget build(BuildContext context) { + final spacing = context.appSpacing; + + final padding = switch (size) { + AppButtonSize.small => EdgeInsets.symmetric( + horizontal: spacing.sm, + vertical: spacing.xxs, + ), + AppButtonSize.medium => EdgeInsets.symmetric( + horizontal: spacing.md, + vertical: spacing.xs, + ), + AppButtonSize.large => EdgeInsets.symmetric( + horizontal: spacing.lg, + vertical: spacing.sm, + ), + }; + + final style = ButtonStyle( + padding: WidgetStatePropertyAll(padding), + ); + + return switch (variant) { + AppButtonVariant.primary => FilledButton( + onPressed: onPressed, + style: style, + child: child, + ), + AppButtonVariant.secondary => FilledButton.tonal( + onPressed: onPressed, + style: style, + child: child, + ), + AppButtonVariant.outline => OutlinedButton( + onPressed: onPressed, + style: style, + child: child, + ), + }; + } +} + +/// Visual variants for [AppButton]. +enum AppButtonVariant { primary, secondary, outline } + +/// Size variants for [AppButton]. +enum AppButtonSize { small, medium, large } +``` + +--- + +## Widget Test Example + +```dart +void main() { + group('AppButton', () { + testWidgets('renders child', (tester) async { + await tester.pumpApp( + AppButton( + onPressed: () {}, + child: const Text('Tap me'), + ), + ); + + expect(find.text('Tap me'), findsOneWidget); + }); + + testWidgets('calls onPressed when tapped', (tester) async { + var tapped = false; + await tester.pumpApp( + AppButton( + onPressed: () => tapped = true, + child: const Text('Tap me'), + ), + ); + + await tester.tap(find.byType(AppButton)); + expect(tapped, isTrue); + }); + + testWidgets('does not call onPressed when disabled', (tester) async { + var tapped = false; + await tester.pumpApp( + AppButton( + onPressed: null, + child: const Text('Disabled'), + ), + ); + + await tester.tap(find.byType(AppButton)); + expect(tapped, isFalse); + }); + }); +} +``` + +--- + +## Widgetbook Catalog + +### pubspec.yaml + +```yaml +name: widgetbook_catalog +description: "Widgetbook catalog for My UI" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + +dependencies: + flutter: + sdk: flutter + my_ui: + path: .. + widgetbook: ^3.7.0 + widgetbook_annotation: ^3.1.0 + +dev_dependencies: + build_runner: ^2.4.7 + flutter_test: + sdk: flutter + very_good_analysis: ^7.0.0 + widgetbook_generator: ^3.7.0 + +flutter: + uses-material-design: true +``` + +### Entry Point + +```dart +import 'package:flutter/material.dart'; +import 'package:widgetbook_catalog/widgetbook/widgetbook.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const WidgetbookApp()); +} +``` + +### WidgetbookApp + +```dart +import 'package:flutter/material.dart'; +import 'package:my_ui/my_ui.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; +import 'package:widgetbook_catalog/widgetbook/widgetbook.directories.g.dart'; +import 'package:widgetbook_catalog/widgetbook/widgets/widgets.dart'; + +@widgetbook.App() +class WidgetbookApp extends StatelessWidget { + const WidgetbookApp({super.key}); + + @override + Widget build(BuildContext context) { + return Widgetbook.material( + directories: directories, + addons: [ + BuilderAddon( + name: 'Decorator', + builder: (context, child) { + return UseCaseDecorator(child: child); + }, + ), + ThemeAddon( + themes: [ + WidgetbookTheme(name: 'Light', data: AppTheme.light), + WidgetbookTheme(name: 'Dark', data: AppTheme.dark), + ], + themeBuilder: (context, theme, child) { + return Theme( + data: theme, + child: DefaultTextStyle( + style: theme.textTheme.bodyMedium ?? const TextStyle(), + child: child, + ), + ); + }, + ), + ], + ); + } +} +``` + +### UseCaseDecorator + +```dart +class UseCaseDecorator extends StatelessWidget { + const UseCaseDecorator({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: SizedBox.expand(child: Material(child: child)), + ); + } +} +``` + +### Use-Case Example + +Each widget gets a dedicated file in `use_cases/` with one or more `@widgetbook.UseCase` annotations: + +```dart +import 'package:flutter/material.dart'; +import 'package:my_ui/my_ui.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase(name: 'primary', type: AppButton) +Widget primary(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + child: const Text('Primary'), + ), + ); + +@widgetbook.UseCase(name: 'secondary', type: AppButton) +Widget secondary(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + variant: AppButtonVariant.secondary, + child: const Text('Secondary'), + ), + ); + +@widgetbook.UseCase(name: 'outline', type: AppButton) +Widget outline(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + variant: AppButtonVariant.outline, + child: const Text('Outline'), + ), + ); + +@widgetbook.UseCase(name: 'disabled', type: AppButton) +Widget disabled(BuildContext context) => const Center( + child: AppButton( + onPressed: null, + child: Text('Disabled'), + ), + ); + +@widgetbook.UseCase(name: 'all sizes', type: AppButton) +Widget allSizes(BuildContext context) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + for (final size in AppButtonSize.values) + AppButton( + onPressed: () {}, + size: size, + child: Text(size.name), + ), + ], + ), + ); +``` From e63d336c2d63280ed25067ad391e8eb09490a8b3 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Mon, 23 Mar 2026 14:07:15 -0300 Subject: [PATCH 05/12] addressing comments --- skills/very-good-ui/SKILL.md | 13 +- skills/very-good-ui/reference.md | 494 ------------------------------- 2 files changed, 5 insertions(+), 502 deletions(-) delete mode 100644 skills/very-good-ui/reference.md diff --git a/skills/very-good-ui/SKILL.md b/skills/very-good-ui/SKILL.md index b75caa2..988a824 100644 --- a/skills/very-good-ui/SKILL.md +++ b/skills/very-good-ui/SKILL.md @@ -1,6 +1,7 @@ --- name: very-good-ui -description: Best practices for building a Flutter UI package on top of Material — custom components, ThemeExtension-based theming, consistent APIs, and widget tests. +description: Best practices for building a Flutter UI package on top of Material — custom components, ThemeExtension-based theming, consistent APIs, and widget tests. Use when user says "create a ui package". Supports app_ui_package template. +allowed-tools: Edit,mcp__very-good-cli__create --- # Very Good UI @@ -198,12 +199,8 @@ widgetbook/ ### Creating the Package -Use Very Good CLI to scaffold the package: +Use the Very Good CLI MCP tool to scaffold the package: -```bash -very_good create flutter_package my_ui --description "A custom Flutter UI package" ``` - -## Additional Resources - -See [reference.md](reference.md) for complete code examples: `ThemeExtension` implementations (`AppColors`, `AppSpacing`), `AppTheme` class with light/dark variants, `BuildContext` extensions, example widget (`AppButton`), widget tests, Widgetbook catalog setup (`pubspec.yaml`, `WidgetbookApp`, `UseCaseDecorator`, use-case files). +mcp__very-good-cli__create(template: "app_ui_package", name: "my_ui", description: "A custom Flutter UI package") +``` diff --git a/skills/very-good-ui/reference.md b/skills/very-good-ui/reference.md deleted file mode 100644 index 3105cf0..0000000 --- a/skills/very-good-ui/reference.md +++ /dev/null @@ -1,494 +0,0 @@ -# Very Good UI — Reference - -Complete code examples for the Very Good UI skill: theming, widgets, testing, and Widgetbook catalog setup. - ---- - -## ThemeExtension: AppColors - -Custom color tokens for values beyond Material's `ColorScheme`: - -```dart -class AppColors extends ThemeExtension { - const AppColors({ - required this.success, - required this.onSuccess, - required this.warning, - required this.onWarning, - required this.info, - required this.onInfo, - }); - - final Color success; - final Color onSuccess; - final Color warning; - final Color onWarning; - final Color info; - final Color onInfo; - - @override - AppColors copyWith({ - Color? success, - Color? onSuccess, - Color? warning, - Color? onWarning, - Color? info, - Color? onInfo, - }) { - return AppColors( - success: success ?? this.success, - onSuccess: onSuccess ?? this.onSuccess, - warning: warning ?? this.warning, - onWarning: onWarning ?? this.onWarning, - info: info ?? this.info, - onInfo: onInfo ?? this.onInfo, - ); - } - - @override - AppColors lerp(AppColors? other, double t) { - if (other is! AppColors) return this; - return AppColors( - success: Color.lerp(success, other.success, t)!, - onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!, - warning: Color.lerp(warning, other.warning, t)!, - onWarning: Color.lerp(onWarning, other.onWarning, t)!, - info: Color.lerp(info, other.info, t)!, - onInfo: Color.lerp(onInfo, other.onInfo, t)!, - ); - } -} -``` - ---- - -## ThemeExtension: AppSpacing - -Spacing scale with consistent token names: - -```dart -class AppSpacing extends ThemeExtension { - const AppSpacing({ - this.xxs = 4, - this.xs = 8, - this.sm = 12, - this.md = 16, - this.lg = 24, - this.xlg = 32, - this.xxlg = 48, - }); - - final double xxs; - final double xs; - final double sm; - final double md; - final double lg; - final double xlg; - final double xxlg; - - @override - AppSpacing copyWith({ - double? xxs, - double? xs, - double? sm, - double? md, - double? lg, - double? xlg, - double? xxlg, - }) { - return AppSpacing( - xxs: xxs ?? this.xxs, - xs: xs ?? this.xs, - sm: sm ?? this.sm, - md: md ?? this.md, - lg: lg ?? this.lg, - xlg: xlg ?? this.xlg, - xxlg: xxlg ?? this.xxlg, - ); - } - - @override - AppSpacing lerp(AppSpacing? other, double t) { - if (other is! AppSpacing) return this; - return AppSpacing( - xxs: lerpDouble(xxs, other.xxs, t)!, - xs: lerpDouble(xs, other.xs, t)!, - sm: lerpDouble(sm, other.sm, t)!, - md: lerpDouble(md, other.md, t)!, - lg: lerpDouble(lg, other.lg, t)!, - xlg: lerpDouble(xlg, other.xlg, t)!, - xxlg: lerpDouble(xxlg, other.xxlg, t)!, - ); - } -} -``` - ---- - -## BuildContext Extensions - -Shorthand access for custom tokens: - -```dart -extension AppThemeBuildContext on BuildContext { - AppColors get appColors => - Theme.of(this).extension()!; - - AppSpacing get appSpacing => - Theme.of(this).extension()!; -} -``` - ---- - -## AppTheme Class - -Composes Material's `ThemeData` with custom extensions for light and dark variants: - -```dart -class AppTheme { - static ThemeData get light { - const appColors = AppColors( - success: Color(0xFF16A34A), - onSuccess: Color(0xFFFFFFFF), - warning: Color(0xFFCA8A04), - onWarning: Color(0xFFFFFFFF), - info: Color(0xFF2563EB), - onInfo: Color(0xFFFFFFFF), - ); - - return ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF4F46E5), - ), - extensions: const [ - appColors, - AppSpacing(), - ], - ); - } - - static ThemeData get dark { - const appColors = AppColors( - success: Color(0xFF4ADE80), - onSuccess: Color(0xFF1C1B1F), - warning: Color(0xFFFACC15), - onWarning: Color(0xFF1C1B1F), - info: Color(0xFF60A5FA), - onInfo: Color(0xFF1C1B1F), - ); - - return ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF4F46E5), - brightness: Brightness.dark, - ), - extensions: const [ - appColors, - AppSpacing(), - ], - ); - } -} -``` - ---- - -## Example Widget: AppButton - -A styled button composing Material's `FilledButton` and `OutlinedButton`: - -```dart -/// A styled button from the UI package. -/// -/// Wraps Material's [FilledButton] and [OutlinedButton] with app-specific -/// sizing and theming. -class AppButton extends StatelessWidget { - /// Creates an [AppButton]. - const AppButton({ - required this.onPressed, - required this.child, - this.variant = AppButtonVariant.primary, - this.size = AppButtonSize.medium, - super.key, - }); - - /// Called when the button is tapped. - final VoidCallback? onPressed; - - /// The button's content, typically a [Text] widget. - final Widget child; - - /// The visual variant of the button. - final AppButtonVariant variant; - - /// The size of the button. - final AppButtonSize size; - - @override - Widget build(BuildContext context) { - final spacing = context.appSpacing; - - final padding = switch (size) { - AppButtonSize.small => EdgeInsets.symmetric( - horizontal: spacing.sm, - vertical: spacing.xxs, - ), - AppButtonSize.medium => EdgeInsets.symmetric( - horizontal: spacing.md, - vertical: spacing.xs, - ), - AppButtonSize.large => EdgeInsets.symmetric( - horizontal: spacing.lg, - vertical: spacing.sm, - ), - }; - - final style = ButtonStyle( - padding: WidgetStatePropertyAll(padding), - ); - - return switch (variant) { - AppButtonVariant.primary => FilledButton( - onPressed: onPressed, - style: style, - child: child, - ), - AppButtonVariant.secondary => FilledButton.tonal( - onPressed: onPressed, - style: style, - child: child, - ), - AppButtonVariant.outline => OutlinedButton( - onPressed: onPressed, - style: style, - child: child, - ), - }; - } -} - -/// Visual variants for [AppButton]. -enum AppButtonVariant { primary, secondary, outline } - -/// Size variants for [AppButton]. -enum AppButtonSize { small, medium, large } -``` - ---- - -## Widget Test Example - -```dart -void main() { - group('AppButton', () { - testWidgets('renders child', (tester) async { - await tester.pumpApp( - AppButton( - onPressed: () {}, - child: const Text('Tap me'), - ), - ); - - expect(find.text('Tap me'), findsOneWidget); - }); - - testWidgets('calls onPressed when tapped', (tester) async { - var tapped = false; - await tester.pumpApp( - AppButton( - onPressed: () => tapped = true, - child: const Text('Tap me'), - ), - ); - - await tester.tap(find.byType(AppButton)); - expect(tapped, isTrue); - }); - - testWidgets('does not call onPressed when disabled', (tester) async { - var tapped = false; - await tester.pumpApp( - AppButton( - onPressed: null, - child: const Text('Disabled'), - ), - ); - - await tester.tap(find.byType(AppButton)); - expect(tapped, isFalse); - }); - }); -} -``` - ---- - -## Widgetbook Catalog - -### pubspec.yaml - -```yaml -name: widgetbook_catalog -description: "Widgetbook catalog for My UI" -publish_to: 'none' -version: 1.0.0+1 - -environment: - sdk: '>=3.2.3 <4.0.0' - -dependencies: - flutter: - sdk: flutter - my_ui: - path: .. - widgetbook: ^3.7.0 - widgetbook_annotation: ^3.1.0 - -dev_dependencies: - build_runner: ^2.4.7 - flutter_test: - sdk: flutter - very_good_analysis: ^7.0.0 - widgetbook_generator: ^3.7.0 - -flutter: - uses-material-design: true -``` - -### Entry Point - -```dart -import 'package:flutter/material.dart'; -import 'package:widgetbook_catalog/widgetbook/widgetbook.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - runApp(const WidgetbookApp()); -} -``` - -### WidgetbookApp - -```dart -import 'package:flutter/material.dart'; -import 'package:my_ui/my_ui.dart'; -import 'package:widgetbook/widgetbook.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; -import 'package:widgetbook_catalog/widgetbook/widgetbook.directories.g.dart'; -import 'package:widgetbook_catalog/widgetbook/widgets/widgets.dart'; - -@widgetbook.App() -class WidgetbookApp extends StatelessWidget { - const WidgetbookApp({super.key}); - - @override - Widget build(BuildContext context) { - return Widgetbook.material( - directories: directories, - addons: [ - BuilderAddon( - name: 'Decorator', - builder: (context, child) { - return UseCaseDecorator(child: child); - }, - ), - ThemeAddon( - themes: [ - WidgetbookTheme(name: 'Light', data: AppTheme.light), - WidgetbookTheme(name: 'Dark', data: AppTheme.dark), - ], - themeBuilder: (context, theme, child) { - return Theme( - data: theme, - child: DefaultTextStyle( - style: theme.textTheme.bodyMedium ?? const TextStyle(), - child: child, - ), - ); - }, - ), - ], - ); - } -} -``` - -### UseCaseDecorator - -```dart -class UseCaseDecorator extends StatelessWidget { - const UseCaseDecorator({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return ColoredBox( - color: colorScheme.surfaceContainerHighest, - child: SizedBox.expand(child: Material(child: child)), - ); - } -} -``` - -### Use-Case Example - -Each widget gets a dedicated file in `use_cases/` with one or more `@widgetbook.UseCase` annotations: - -```dart -import 'package:flutter/material.dart'; -import 'package:my_ui/my_ui.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; - -@widgetbook.UseCase(name: 'primary', type: AppButton) -Widget primary(BuildContext context) => Center( - child: AppButton( - onPressed: () {}, - child: const Text('Primary'), - ), - ); - -@widgetbook.UseCase(name: 'secondary', type: AppButton) -Widget secondary(BuildContext context) => Center( - child: AppButton( - onPressed: () {}, - variant: AppButtonVariant.secondary, - child: const Text('Secondary'), - ), - ); - -@widgetbook.UseCase(name: 'outline', type: AppButton) -Widget outline(BuildContext context) => Center( - child: AppButton( - onPressed: () {}, - variant: AppButtonVariant.outline, - child: const Text('Outline'), - ), - ); - -@widgetbook.UseCase(name: 'disabled', type: AppButton) -Widget disabled(BuildContext context) => const Center( - child: AppButton( - onPressed: null, - child: Text('Disabled'), - ), - ); - -@widgetbook.UseCase(name: 'all sizes', type: AppButton) -Widget allSizes(BuildContext context) => Center( - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - for (final size in AppButtonSize.values) - AppButton( - onPressed: () {}, - size: size, - child: Text(size.name), - ), - ], - ), - ); -``` From 60629264e6f469605465b840c775bd89f5e3ff0c Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 25 Mar 2026 10:10:22 -0300 Subject: [PATCH 06/12] refactor: rename very-good-ui skill to vgv-ui-package and reconcile theming overlap Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude-plugin/plugin.json | 2 +- CLAUDE.md | 3 +-- README.md | 4 ++-- skills/{very-good-ui => ui-package}/SKILL.md | 14 ++++++++------ 4 files changed, 12 insertions(+), 11 deletions(-) rename skills/{very-good-ui => ui-package}/SKILL.md (88%) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3f283b6..f709157 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -46,7 +46,7 @@ "testing", "theming", "very-good-cli", - "very-good-ui", + "ui-package", "wcag", "widget-testing", "sdk-upgrade", diff --git a/CLAUDE.md b/CLAUDE.md index ee6425a..894304d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,7 @@ skills/ static-security/reference.md testing/SKILL.md testing/reference.md - very-good-ui/SKILL.md - very-good-ui/reference.md + ui-package/SKILL.md ``` ## Skill File Format diff --git a/README.md b/README.md index 6e73069..ada72f6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ For more details, see the [Very Good Claude Marketplace][marketplace_link]. | [**Bloc**](skills/bloc/SKILL.md) | State management with Bloc/Cubit — sealed events & states, `BlocProvider`/`BlocBuilder` widgets, event transformers, and testing with `blocTest()` & `mocktail` | | [**Layered Architecture**](skills/layered-architecture/SKILL.md) | VGV layered architecture — four-layer package structure (Data, Repository, Business Logic, Presentation), dependency rules, data flow, and bootstrap wiring | | [**Security**](skills/static-security/SKILL.md) | Flutter-specific static security review — secrets management, `flutter_secure_storage`, certificate pinning, `Random.secure()`, `formz` validation, dependency vulnerability scanning with `osv-scanner`, and OWASP Mobile Top 10 guidance | -| [**Very Good UI**](skills/very-good-ui/SKILL.md) | Flutter UI package creation — custom widget libraries with theming via `InheritedWidget`, design tokens, barrel file exports, golden tests, widget tests, gallery apps, and consistent API conventions | +| [**UI Package**](skills/ui-package/SKILL.md) | Flutter UI package creation — custom widget libraries with `ThemeExtension`-based theming, design tokens, barrel file exports, widget tests, Widgetbook catalog, and consistent API conventions | | [**License Compliance**](skills/license-compliance/SKILL.md) | Dependency license auditing — categorizes licenses (permissive, weak/strong copyleft, unknown), flags non-compliant or missing licenses, and produces a structured compliance report using Very Good CLI | | [**Dart/Flutter SDK Upgrade**](skills/dart-flutter-sdk-upgrade/SKILL.md) | Bump Dart and Flutter SDK constraints across packages — CI workflow versions, pubspec.yaml environment constraints, and PR preparation for SDK upgrades | @@ -77,7 +77,7 @@ You can also invoke skills directly as slash commands: /vgv-navigation /vgv-static-security /vgv-testing -/vgv-very-good-ui +/vgv-ui-package /vgv-license-compliance /vgv-dart-flutter-sdk-upgrade ``` diff --git a/skills/very-good-ui/SKILL.md b/skills/ui-package/SKILL.md similarity index 88% rename from skills/very-good-ui/SKILL.md rename to skills/ui-package/SKILL.md index 988a824..71ad2dd 100644 --- a/skills/very-good-ui/SKILL.md +++ b/skills/ui-package/SKILL.md @@ -1,13 +1,15 @@ --- -name: very-good-ui +name: vgv-ui-package description: Best practices for building a Flutter UI package on top of Material — custom components, ThemeExtension-based theming, consistent APIs, and widget tests. Use when user says "create a ui package". Supports app_ui_package template. allowed-tools: Edit,mcp__very-good-cli__create --- -# Very Good UI +# UI Package Best practices for creating a Flutter UI package — a reusable widget library that builds on top of `package:flutter/material.dart`, extending it with app-specific components, custom design tokens via `ThemeExtension`, and a consistent API surface. +> **Theming foundation:** This skill focuses on UI package structure, widget APIs, and testing. For foundational Material 3 theming (`ColorScheme`, `TextTheme`, component themes, spacing constants, light/dark mode), see the **Material Theming** skill (`/vgv-material-theming`). The two skills are complementary — Material Theming covers how to set up and use `ThemeData`; this skill covers how to extend it with `ThemeExtension` tokens and package reusable widgets around it. + ## Core Standards Apply these standards to ALL UI package work: @@ -15,7 +17,7 @@ Apply these standards to ALL UI package work: - **Build on Material** — depend on `flutter/material.dart` and compose Material widgets; do not rebuild primitives that Material already provides - **One widget per file** — each public widget lives in its own file named after the widget in snake_case (e.g., `app_button.dart`) - **Barrel file for public API** — expose all public widgets and theme classes through a single barrel file (e.g., `lib/my_ui.dart`) that also re-exports `material.dart` -- **Extend theming with `ThemeExtension`** — use Material's `ThemeData`, `ColorScheme`, and `TextTheme` as the base; add app-specific tokens (spacing, custom colors) via `ThemeExtension` +- **Extend theming with `ThemeExtension`** — use Material's `ThemeData`, `ColorScheme`, and `TextTheme` as the base (see Material Theming skill); add app-specific tokens (spacing, custom colors) via `ThemeExtension` - **Every widget has a corresponding widget test** — behavioral tests verify interactions, callbacks, and state changes - **Prefix all public classes** — use a consistent prefix (e.g., `App`, `Vg`) to avoid naming collisions with Material widgets - **Use `const` constructors everywhere possible** — all widget constructors must be `const` when feasible @@ -54,9 +56,9 @@ my_ui/ └── pubspec.yaml ``` -## Theming System +## ThemeExtension Tokens -Use Material's `ThemeData`, `ColorScheme`, and `TextTheme` as the base. Add app-specific tokens via `ThemeExtension` — only for values Material does not cover (e.g., success, warning, info colors; spacing scale). +The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, spacing constants) is covered by the **Material Theming** skill. This section covers the `ThemeExtension` layer unique to UI packages — custom tokens for values Material does not provide (e.g., success/warning/info colors, spacing scale as animatable values). ### Key Classes @@ -64,7 +66,7 @@ Use Material's `ThemeData`, `ColorScheme`, and `TextTheme` as the base. Add app- | ----- | ------- | | `AppColors extends ThemeExtension` | Custom color tokens beyond `ColorScheme` (success, warning, info + on-variants) | | `AppSpacing extends ThemeExtension` | Spacing scale (xxs through xxlg) with `copyWith` and `lerp` | -| `AppTheme` | Composes `ThemeData` with `ColorScheme.fromSeed`, custom extensions, for light and dark variants | +| `AppTheme` | Composes `ThemeData` with `ColorScheme.fromSeed` + custom extensions, for light and dark variants | | `AppThemeBuildContext` extension | Shorthand `context.appColors` and `context.appSpacing` | Every `ThemeExtension` must implement `copyWith` and `lerp` for theme animation support. From 92cdfc731e14c4c4473857dfc63daecbeb68cf64 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 25 Mar 2026 10:11:59 -0300 Subject: [PATCH 07/12] chore: add animatable to cspell dictionary Co-Authored-By: Claude Opus 4.6 (1M context) --- config/cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config/cspell.json b/config/cspell.json index 8c58b5d..254c41a 100644 --- a/config/cspell.json +++ b/config/cspell.json @@ -1,6 +1,7 @@ { "language": "en", "words": [ + "animatable", "Bidirectionality", "Bienvenido", "bypassable", From 60524ddc8eb1f644e56267842ca5d3b3385c20e2 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 25 Mar 2026 10:12:33 -0300 Subject: [PATCH 08/12] chore: replace animatable with real words Co-Authored-By: Claude Opus 4.6 (1M context) --- config/cspell.json | 1 - skills/ui-package/SKILL.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/config/cspell.json b/config/cspell.json index 254c41a..8c58b5d 100644 --- a/config/cspell.json +++ b/config/cspell.json @@ -1,7 +1,6 @@ { "language": "en", "words": [ - "animatable", "Bidirectionality", "Bienvenido", "bypassable", diff --git a/skills/ui-package/SKILL.md b/skills/ui-package/SKILL.md index 71ad2dd..6c222cb 100644 --- a/skills/ui-package/SKILL.md +++ b/skills/ui-package/SKILL.md @@ -58,7 +58,7 @@ my_ui/ ## ThemeExtension Tokens -The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, spacing constants) is covered by the **Material Theming** skill. This section covers the `ThemeExtension` layer unique to UI packages — custom tokens for values Material does not provide (e.g., success/warning/info colors, spacing scale as animatable values). +The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, spacing constants) is covered by the **Material Theming** skill. This section covers the `ThemeExtension` layer unique to UI packages — custom tokens for values Material does not provide (e.g., success/warning/info colors, spacing scale with animation support). ### Key Classes From 0b6548d564447d5e95b4bb53f81117f3333a1962 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 31 Mar 2026 11:36:42 -0300 Subject: [PATCH 09/12] refactor(ui-package): move detailed examples to reference.md Move ThemeExtension tokens, testing section, barrel file example, Widgetbook catalog, and common workflows to reference.md to keep SKILL.md focused on enforcement rules. Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/ui-package/SKILL.md | 123 +------------------------------- skills/ui-package/reference.md | 126 +++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 122 deletions(-) create mode 100644 skills/ui-package/reference.md diff --git a/skills/ui-package/SKILL.md b/skills/ui-package/SKILL.md index 6c222cb..62ed99e 100644 --- a/skills/ui-package/SKILL.md +++ b/skills/ui-package/SKILL.md @@ -56,21 +56,6 @@ my_ui/ └── pubspec.yaml ``` -## ThemeExtension Tokens - -The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, spacing constants) is covered by the **Material Theming** skill. This section covers the `ThemeExtension` layer unique to UI packages — custom tokens for values Material does not provide (e.g., success/warning/info colors, spacing scale with animation support). - -### Key Classes - -| Class | Purpose | -| ----- | ------- | -| `AppColors extends ThemeExtension` | Custom color tokens beyond `ColorScheme` (success, warning, info + on-variants) | -| `AppSpacing extends ThemeExtension` | Spacing scale (xxs through xxlg) with `copyWith` and `lerp` | -| `AppTheme` | Composes `ThemeData` with `ColorScheme.fromSeed` + custom extensions, for light and dark variants | -| `AppThemeBuildContext` extension | Shorthand `context.appColors` and `context.appSpacing` | - -Every `ThemeExtension` must implement `copyWith` and `lerp` for theme animation support. - ## Building Widgets ### Widget API Guidelines @@ -82,93 +67,6 @@ Every `ThemeExtension` must implement `copyWith` and `lerp` for theme animation - Expose callbacks with `ValueChanged` or `VoidCallback` — do not use raw `Function` - Use `Widget?` for optional slot-based composition (leading, trailing icons, etc.) -## Testing - -### Test Helper - -Create a `pumpApp` helper that wraps widgets in a `MaterialApp` with the full theme: - -```dart -extension PumpApp on WidgetTester { - Future pumpApp( - Widget widget, { - ThemeData? theme, - }) { - return pumpWidget( - MaterialApp( - theme: theme ?? AppTheme.light, - home: Scaffold(body: widget), - ), - ); - } -} -``` - -### Test Patterns - -- Test rendering: verify the widget shows the expected content -- Test interactions: verify callbacks fire on tap/input -- Test disabled state: verify callbacks do not fire when `onPressed` is `null` -- Test all variants: cover each enum value (variant, size, etc.) - -## Barrel File - -Re-export Material and the full public API through a single barrel file: - -```dart -/// My UI — a custom Flutter widget library built on Material. -library; - -export 'package:flutter/material.dart'; - -export 'src/extensions/build_context_extensions.dart'; -export 'src/theme/app_colors.dart'; -export 'src/theme/app_spacing.dart'; -export 'src/theme/app_theme.dart'; -export 'src/widgets/app_button.dart'; -export 'src/widgets/app_card.dart'; -export 'src/widgets/app_text_field.dart'; -``` - -## Widgetbook Catalog - -The UI package includes a `widgetbook/` submodule — a standalone Flutter app powered by Widgetbook that serves as both a **developer sandbox** for building widgets in isolation and a **showcase** for browsing every widget in the package. The submodule package name in `pubspec.yaml` is `widgetbook_catalog`. - -### Catalog Structure - -``` -widgetbook/ -├── lib/ -│ ├── main.dart # Entry point — runs WidgetbookApp -│ └── widgetbook/ -│ ├── widgetbook.dart # WidgetbookApp widget with addons -│ ├── widgetbook.directories.g.dart # Generated — do not edit -│ ├── use_cases/ -│ │ ├── app_button.dart # Use cases for AppButton -│ │ ├── app_card.dart -│ │ └── ... # One file per widget -│ └── widgets/ -│ ├── widgets.dart # Barrel file for catalog helpers -│ └── use_case_decorator.dart # Wrapper for consistent presentation -├── pubspec.yaml # Package name: widgetbook_catalog -├── analysis_options.yaml -└── .gitignore -``` - -### Key Concepts - -- **Use cases**: top-level functions annotated with `@widgetbook.UseCase(name:, type:)`, one file per widget in `use_cases/` -- **Use-case decorator**: a `UseCaseDecorator` widget that wraps every use case with a consistent background -- **Theme addon**: `ThemeAddon` wired to `AppTheme.light` and `AppTheme.dark` for switching themes in the catalog -- **Code generation**: Widgetbook uses `build_runner` to scan annotations and generate `widgetbook.directories.g.dart` - -### Commands - -| Command | Purpose | -| ------- | ------- | -| `cd widgetbook && dart run build_runner build --delete-conflicting-outputs` | Regenerate use-case directories after adding/modifying use cases | -| `cd widgetbook && flutter run -d chrome` | Run the catalog locally | - ## Anti-Patterns | Anti-Pattern | Correct Approach | @@ -180,26 +78,7 @@ widgetbook/ | Using `dynamic` or `Object` for callback types | Use `VoidCallback`, `ValueChanged`, or specific function typedefs | | Exposing internal implementation files directly | Use a barrel file; keep all files under `src/` private | -## Common Workflows - -### Adding a New Widget - -1. Create `lib/src/widgets/app_.dart` with a `const` constructor and documentation -2. Compose Material widgets internally; read custom tokens via `context.appColors` / `context.appSpacing` -3. Export the file from the barrel file (`lib/my_ui.dart`) -4. Create `test/src/widgets/app__test.dart` with widget tests -5. Add use cases in `widgetbook/lib/widgetbook/use_cases/app_.dart` covering all variants -6. Re-run `dart run build_runner build --delete-conflicting-outputs` in `widgetbook/` - -### Adding a New Custom Token - -1. Add the token to the appropriate `ThemeExtension` class (`AppColors` or `AppSpacing`) -2. Update `copyWith` and `lerp` methods -3. Update `AppTheme.light` and `AppTheme.dark` to include the new token value -4. Update existing tests that construct the extension directly -5. Use the new token in widgets via the `BuildContext` extension - -### Creating the Package +## Creating the Package Use the Very Good CLI MCP tool to scaffold the package: diff --git a/skills/ui-package/reference.md b/skills/ui-package/reference.md new file mode 100644 index 0000000..04fa573 --- /dev/null +++ b/skills/ui-package/reference.md @@ -0,0 +1,126 @@ +# UI Package — Reference + +Extended examples, testing patterns, and common workflows for the UI Package skill. + +--- + +## ThemeExtension Tokens + +The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, spacing constants) is covered by the **Material Theming** skill. This section covers the `ThemeExtension` layer unique to UI packages — custom tokens for values Material does not provide (e.g., success/warning/info colors, spacing scale with animation support). + +### Key Classes + +| Class | Purpose | +| ----- | ------- | +| `AppColors extends ThemeExtension` | Custom color tokens beyond `ColorScheme` (success, warning, info + on-variants) | +| `AppSpacing extends ThemeExtension` | Spacing scale (xxs through xxlg) with `copyWith` and `lerp` | +| `AppTheme` | Composes `ThemeData` with `ColorScheme.fromSeed` + custom extensions, for light and dark variants | +| `AppThemeBuildContext` extension | Shorthand `context.appColors` and `context.appSpacing` | + +Every `ThemeExtension` must implement `copyWith` and `lerp` for theme animation support. + +## Testing + +### Test Helper + +Create a `pumpApp` helper that wraps widgets in a `MaterialApp` with the full theme: + +```dart +extension PumpApp on WidgetTester { + Future pumpApp( + Widget widget, { + ThemeData? theme, + }) { + return pumpWidget( + MaterialApp( + theme: theme ?? AppTheme.light, + home: Scaffold(body: widget), + ), + ); + } +} +``` + +### Test Patterns + +- Test rendering: verify the widget shows the expected content +- Test interactions: verify callbacks fire on tap/input +- Test disabled state: verify callbacks do not fire when `onPressed` is `null` +- Test all variants: cover each enum value (variant, size, etc.) + +## Barrel File + +Re-export Material and the full public API through a single barrel file: + +```dart +/// My UI — a custom Flutter widget library built on Material. +library; + +export 'package:flutter/material.dart'; + +export 'src/extensions/build_context_extensions.dart'; +export 'src/theme/app_colors.dart'; +export 'src/theme/app_spacing.dart'; +export 'src/theme/app_theme.dart'; +export 'src/widgets/app_button.dart'; +export 'src/widgets/app_card.dart'; +export 'src/widgets/app_text_field.dart'; +``` + +## Widgetbook Catalog + +The UI package includes a `widgetbook/` submodule — a standalone Flutter app powered by Widgetbook that serves as both a **developer sandbox** for building widgets in isolation and a **showcase** for browsing every widget in the package. The submodule package name in `pubspec.yaml` is `widgetbook_catalog`. + +### Catalog Structure + +``` +widgetbook/ +├── lib/ +│ ├── main.dart # Entry point — runs WidgetbookApp +│ └── widgetbook/ +│ ├── widgetbook.dart # WidgetbookApp widget with addons +│ ├── widgetbook.directories.g.dart # Generated — do not edit +│ ├── use_cases/ +│ │ ├── app_button.dart # Use cases for AppButton +│ │ ├── app_card.dart +│ │ └── ... # One file per widget +│ └── widgets/ +│ ├── widgets.dart # Barrel file for catalog helpers +│ └── use_case_decorator.dart # Wrapper for consistent presentation +├── pubspec.yaml # Package name: widgetbook_catalog +├── analysis_options.yaml +└── .gitignore +``` + +### Key Concepts + +- **Use cases**: top-level functions annotated with `@widgetbook.UseCase(name:, type:)`, one file per widget in `use_cases/` +- **Use-case decorator**: a `UseCaseDecorator` widget that wraps every use case with a consistent background +- **Theme addon**: `ThemeAddon` wired to `AppTheme.light` and `AppTheme.dark` for switching themes in the catalog +- **Code generation**: Widgetbook uses `build_runner` to scan annotations and generate `widgetbook.directories.g.dart` + +### Commands + +| Command | Purpose | +| ------- | ------- | +| `cd widgetbook && dart run build_runner build --delete-conflicting-outputs` | Regenerate use-case directories after adding/modifying use cases | +| `cd widgetbook && flutter run -d chrome` | Run the catalog locally | + +## Common Workflows + +### Adding a New Widget + +1. Create `lib/src/widgets/app_.dart` with a `const` constructor and documentation +2. Compose Material widgets internally; read custom tokens via `context.appColors` / `context.appSpacing` +3. Export the file from the barrel file (`lib/my_ui.dart`) +4. Create `test/src/widgets/app__test.dart` with widget tests +5. Add use cases in `widgetbook/lib/widgetbook/use_cases/app_.dart` covering all variants +6. Re-run `dart run build_runner build --delete-conflicting-outputs` in `widgetbook/` + +### Adding a New Custom Token + +1. Add the token to the appropriate `ThemeExtension` class (`AppColors` or `AppSpacing`) +2. Update `copyWith` and `lerp` methods +3. Update `AppTheme.light` and `AppTheme.dark` to include the new token value +4. Update existing tests that construct the extension directly +5. Use the new token in widgets via the `BuildContext` extension From e498c3300fae62cfa3309b6782073bb2ad5245c7 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 31 Mar 2026 11:39:12 -0300 Subject: [PATCH 10/12] fix: add language specifiers to fenced code blocks Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/ui-package/SKILL.md | 4 ++-- skills/ui-package/reference.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/ui-package/SKILL.md b/skills/ui-package/SKILL.md index 62ed99e..5418423 100644 --- a/skills/ui-package/SKILL.md +++ b/skills/ui-package/SKILL.md @@ -25,7 +25,7 @@ Apply these standards to ALL UI package work: ## Package Structure -``` +```text my_ui/ ├── lib/ │ ├── my_ui.dart # Barrel file — re-exports material.dart + all public API @@ -82,6 +82,6 @@ my_ui/ Use the Very Good CLI MCP tool to scaffold the package: -``` +```text mcp__very-good-cli__create(template: "app_ui_package", name: "my_ui", description: "A custom Flutter UI package") ``` diff --git a/skills/ui-package/reference.md b/skills/ui-package/reference.md index 04fa573..c7463b6 100644 --- a/skills/ui-package/reference.md +++ b/skills/ui-package/reference.md @@ -73,7 +73,7 @@ The UI package includes a `widgetbook/` submodule — a standalone Flutter app p ### Catalog Structure -``` +```text widgetbook/ ├── lib/ │ ├── main.dart # Entry point — runs WidgetbookApp From 3aeaf0efc6c5a4059d4bc36126eee0a7e563d78d Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 31 Mar 2026 11:43:44 -0300 Subject: [PATCH 11/12] chore: remove duplicated content from reference.md Remove repeated preambles, directory trees, and explanations that already exist in SKILL.md. Keep only concrete examples, key class tables, and step-by-step workflows. Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/ui-package/reference.md | 48 +++------------------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/skills/ui-package/reference.md b/skills/ui-package/reference.md index c7463b6..129a772 100644 --- a/skills/ui-package/reference.md +++ b/skills/ui-package/reference.md @@ -1,14 +1,10 @@ # UI Package — Reference -Extended examples, testing patterns, and common workflows for the UI Package skill. +Concrete examples and step-by-step workflows for the UI Package skill. --- -## ThemeExtension Tokens - -The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, spacing constants) is covered by the **Material Theming** skill. This section covers the `ThemeExtension` layer unique to UI packages — custom tokens for values Material does not provide (e.g., success/warning/info colors, spacing scale with animation support). - -### Key Classes +## ThemeExtension Key Classes | Class | Purpose | | ----- | ------- | @@ -19,11 +15,7 @@ The base theme setup (`ThemeData`, `ColorScheme`, `TextTheme`, component themes, Every `ThemeExtension` must implement `copyWith` and `lerp` for theme animation support. -## Testing - -### Test Helper - -Create a `pumpApp` helper that wraps widgets in a `MaterialApp` with the full theme: +## Test Helper ```dart extension PumpApp on WidgetTester { @@ -41,16 +33,7 @@ extension PumpApp on WidgetTester { } ``` -### Test Patterns - -- Test rendering: verify the widget shows the expected content -- Test interactions: verify callbacks fire on tap/input -- Test disabled state: verify callbacks do not fire when `onPressed` is `null` -- Test all variants: cover each enum value (variant, size, etc.) - -## Barrel File - -Re-export Material and the full public API through a single barrel file: +## Barrel File Example ```dart /// My UI — a custom Flutter widget library built on Material. @@ -69,29 +52,6 @@ export 'src/widgets/app_text_field.dart'; ## Widgetbook Catalog -The UI package includes a `widgetbook/` submodule — a standalone Flutter app powered by Widgetbook that serves as both a **developer sandbox** for building widgets in isolation and a **showcase** for browsing every widget in the package. The submodule package name in `pubspec.yaml` is `widgetbook_catalog`. - -### Catalog Structure - -```text -widgetbook/ -├── lib/ -│ ├── main.dart # Entry point — runs WidgetbookApp -│ └── widgetbook/ -│ ├── widgetbook.dart # WidgetbookApp widget with addons -│ ├── widgetbook.directories.g.dart # Generated — do not edit -│ ├── use_cases/ -│ │ ├── app_button.dart # Use cases for AppButton -│ │ ├── app_card.dart -│ │ └── ... # One file per widget -│ └── widgets/ -│ ├── widgets.dart # Barrel file for catalog helpers -│ └── use_case_decorator.dart # Wrapper for consistent presentation -├── pubspec.yaml # Package name: widgetbook_catalog -├── analysis_options.yaml -└── .gitignore -``` - ### Key Concepts - **Use cases**: top-level functions annotated with `@widgetbook.UseCase(name:, type:)`, one file per widget in `use_cases/` From d19d00961420141f82e6eddc54238cc81596ebff Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 31 Mar 2026 11:45:46 -0300 Subject: [PATCH 12/12] chore: simplify creating the package section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove explicit MCP call syntax — Claude already knows how to invoke the tool from MCP. Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/ui-package/SKILL.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skills/ui-package/SKILL.md b/skills/ui-package/SKILL.md index 5418423..1ee2cd8 100644 --- a/skills/ui-package/SKILL.md +++ b/skills/ui-package/SKILL.md @@ -80,8 +80,4 @@ my_ui/ ## Creating the Package -Use the Very Good CLI MCP tool to scaffold the package: - -```text -mcp__very-good-cli__create(template: "app_ui_package", name: "my_ui", description: "A custom Flutter UI package") -``` +Use the Very Good CLI MCP tool to scaffold the `app_ui_package`.