Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"testing",
"theming",
"very-good-cli",
"ui-package",
"wcag",
"widget-testing",
"sdk-upgrade",
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ skills/
static-security/reference.md
testing/SKILL.md
testing/reference.md
ui-package/SKILL.md
```

## Skill File Format
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| [**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 |

Expand Down Expand Up @@ -76,6 +77,7 @@ You can also invoke skills directly as slash commands:
/vgv-navigation
/vgv-static-security
/vgv-testing
/vgv-ui-package
/vgv-license-compliance
/vgv-dart-flutter-sdk-upgrade
```
Expand Down
4 changes: 4 additions & 0 deletions config/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"Bidirectionality",
"Bienvenido",
"bypassable",
"dartdoc",
"CSPRNG",
"dismissable",
"elemento",
Expand All @@ -14,6 +15,7 @@
"GHSA",
"goldens",
"hoverable",
"lerp",
"jailbroken",
"LTRB",
"mapbox",
Expand All @@ -26,6 +28,8 @@
"serialization",
"stdio",
"WCAG",
"widgetbook",
"Widgetbook",
"xxlg"
],
"flagWords": []
Expand Down
83 changes: 83 additions & 0 deletions skills/ui-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
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
---

# 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:

- **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 (see Material Theming skill); add app-specific tokens (spacing, custom colors) via `ThemeExtension<T>`
- **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

```text
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
├── widgetbook/ # Widgetbook catalog submodule (sandbox + showcase)
│ └── ...
└── pubspec.yaml
```

## 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<T>` or `VoidCallback` — do not use raw `Function`
- Use `Widget?` for optional slot-based composition (leading, trailing icons, etc.)

## 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 does not cover (e.g., success, warning, info) |
| Using `dynamic` or `Object` for callback types | Use `VoidCallback`, `ValueChanged<T>`, or specific function typedefs |
| Exposing internal implementation files directly | Use a barrel file; keep all files under `src/` private |

## Creating the Package

Use the Very Good CLI MCP tool to scaffold the `app_ui_package`.
86 changes: 86 additions & 0 deletions skills/ui-package/reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# UI Package — Reference

Concrete examples and step-by-step workflows for the UI Package skill.

---

## ThemeExtension Key Classes

| Class | Purpose |
| ----- | ------- |
| `AppColors extends ThemeExtension<AppColors>` | Custom color tokens beyond `ColorScheme` (success, warning, info + on-variants) |
| `AppSpacing extends ThemeExtension<AppSpacing>` | 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.

## Test Helper

```dart
extension PumpApp on WidgetTester {
Future<void> pumpApp(
Widget widget, {
ThemeData? theme,
}) {
return pumpWidget(
MaterialApp(
theme: theme ?? AppTheme.light,
home: Scaffold(body: widget),
),
);
}
}
```

## Barrel File Example

```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

### 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_<name>.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_<name>_test.dart` with widget tests
5. Add use cases in `widgetbook/lib/widgetbook/use_cases/app_<name>.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
Loading