Skip to content

Commit 7fc7955

Browse files
committed
adding widgetbook part
1 parent f61a631 commit 7fc7955

1 file changed

Lines changed: 219 additions & 1 deletion

File tree

skills/very-good-ui/SKILL.md

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ my_ui/
4848
│ │ └── ...
4949
│ └── helpers/
5050
│ └── pump_app.dart # Test helper wrapping widgets in MaterialApp + theme
51-
├── gallery/ # Optional: standalone app showcasing widgets
51+
├── widgetbook/ # Widgetbook catalog submodule (sandbox + showcase)
5252
│ └── ...
5353
└── pubspec.yaml
5454
```
@@ -411,6 +411,222 @@ export 'src/widgets/app_card.dart';
411411
export 'src/widgets/app_text_field.dart';
412412
```
413413

414+
## Widgetbook Catalog
415+
416+
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.
417+
418+
### Catalog Structure
419+
420+
```
421+
my_ui/
422+
├── lib/
423+
│ └── ... # UI package source
424+
├── widgetbook/ # Catalog submodule
425+
│ ├── lib/
426+
│ │ ├── main.dart # Entry point — runs WidgetbookApp
427+
│ │ └── widgetbook/
428+
│ │ ├── widgetbook.dart # WidgetbookApp widget with addons
429+
│ │ ├── widgetbook.directories.g.dart # Generated — do not edit
430+
│ │ ├── use_cases/
431+
│ │ │ ├── app_button.dart # Use cases for AppButton
432+
│ │ │ ├── app_card.dart
433+
│ │ │ └── ... # One file per widget
434+
│ │ └── widgets/
435+
│ │ ├── widgets.dart # Barrel file for catalog helpers
436+
│ │ └── use_case_decorator.dart # Wrapper for consistent presentation
437+
│ ├── pubspec.yaml # Package name: widgetbook_catalog
438+
│ ├── analysis_options.yaml
439+
│ └── .gitignore
440+
└── pubspec.yaml
441+
```
442+
443+
### pubspec.yaml
444+
445+
The catalog depends on the UI package via a path reference and uses Widgetbook's annotation + code generation approach:
446+
447+
```yaml
448+
name: widgetbook_catalog
449+
description: "Widgetbook catalog for My UI"
450+
publish_to: 'none'
451+
version: 1.0.0+1
452+
453+
environment:
454+
sdk: '>=3.2.3 <4.0.0'
455+
456+
dependencies:
457+
flutter:
458+
sdk: flutter
459+
my_ui:
460+
path: ..
461+
widgetbook: ^3.7.0
462+
widgetbook_annotation: ^3.1.0
463+
464+
dev_dependencies:
465+
build_runner: ^2.4.7
466+
flutter_test:
467+
sdk: flutter
468+
very_good_analysis: ^7.0.0
469+
widgetbook_generator: ^3.7.0
470+
471+
flutter:
472+
uses-material-design: true
473+
```
474+
475+
### Entry Point
476+
477+
```dart
478+
import 'package:flutter/material.dart';
479+
import 'package:widgetbook_catalog/widgetbook/widgetbook.dart';
480+
481+
void main() {
482+
WidgetsFlutterBinding.ensureInitialized();
483+
runApp(const WidgetbookApp());
484+
}
485+
```
486+
487+
### WidgetbookApp
488+
489+
The root widget configures Widgetbook with theme addons and a use-case decorator:
490+
491+
```dart
492+
import 'package:flutter/material.dart';
493+
import 'package:my_ui/my_ui.dart';
494+
import 'package:widgetbook/widgetbook.dart';
495+
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
496+
import 'package:widgetbook_catalog/widgetbook/widgetbook.directories.g.dart';
497+
import 'package:widgetbook_catalog/widgetbook/widgets/widgets.dart';
498+
499+
@widgetbook.App()
500+
class WidgetbookApp extends StatelessWidget {
501+
const WidgetbookApp({super.key});
502+
503+
@override
504+
Widget build(BuildContext context) {
505+
return Widgetbook.material(
506+
directories: directories,
507+
addons: [
508+
BuilderAddon(
509+
name: 'Decorator',
510+
builder: (context, child) {
511+
return UseCaseDecorator(child: child);
512+
},
513+
),
514+
ThemeAddon(
515+
themes: [
516+
WidgetbookTheme(name: 'Light', data: AppTheme.light),
517+
WidgetbookTheme(name: 'Dark', data: AppTheme.dark),
518+
],
519+
themeBuilder: (context, theme, child) {
520+
return Theme(
521+
data: theme,
522+
child: DefaultTextStyle(
523+
style: theme.textTheme.bodyMedium ?? const TextStyle(),
524+
child: child,
525+
),
526+
);
527+
},
528+
),
529+
],
530+
);
531+
}
532+
}
533+
```
534+
535+
### Use-Case Decorator
536+
537+
A wrapper widget that provides a consistent background for all use cases:
538+
539+
```dart
540+
class UseCaseDecorator extends StatelessWidget {
541+
const UseCaseDecorator({required this.child, super.key});
542+
543+
final Widget child;
544+
545+
@override
546+
Widget build(BuildContext context) {
547+
final colorScheme = Theme.of(context).colorScheme;
548+
return ColoredBox(
549+
color: colorScheme.surfaceContainerHighest,
550+
child: SizedBox.expand(child: Material(child: child)),
551+
);
552+
}
553+
}
554+
```
555+
556+
### Writing Use Cases
557+
558+
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`:
559+
560+
```dart
561+
import 'package:flutter/material.dart';
562+
import 'package:my_ui/my_ui.dart';
563+
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
564+
565+
@widgetbook.UseCase(name: 'primary', type: AppButton)
566+
Widget primary(BuildContext context) => Center(
567+
child: AppButton(
568+
onPressed: () {},
569+
child: const Text('Primary'),
570+
),
571+
);
572+
573+
@widgetbook.UseCase(name: 'secondary', type: AppButton)
574+
Widget secondary(BuildContext context) => Center(
575+
child: AppButton(
576+
onPressed: () {},
577+
variant: AppButtonVariant.secondary,
578+
child: const Text('Secondary'),
579+
),
580+
);
581+
582+
@widgetbook.UseCase(name: 'outline', type: AppButton)
583+
Widget outline(BuildContext context) => Center(
584+
child: AppButton(
585+
onPressed: () {},
586+
variant: AppButtonVariant.outline,
587+
child: const Text('Outline'),
588+
),
589+
);
590+
591+
@widgetbook.UseCase(name: 'disabled', type: AppButton)
592+
Widget disabled(BuildContext context) => const Center(
593+
child: AppButton(
594+
onPressed: null,
595+
child: Text('Disabled'),
596+
),
597+
);
598+
599+
@widgetbook.UseCase(name: 'all sizes', type: AppButton)
600+
Widget allSizes(BuildContext context) => Center(
601+
child: Column(
602+
mainAxisSize: MainAxisSize.min,
603+
spacing: 8,
604+
children: [
605+
for (final size in AppButtonSize.values)
606+
AppButton(
607+
onPressed: () {},
608+
size: size,
609+
child: Text(size.name),
610+
),
611+
],
612+
),
613+
);
614+
```
615+
616+
### Code Generation
617+
618+
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:
619+
620+
```bash
621+
cd widgetbook && dart run build_runner build --delete-conflicting-outputs
622+
```
623+
624+
### Running the Catalog
625+
626+
```bash
627+
cd widgetbook && flutter run -d chrome
628+
```
629+
414630
## Anti-Patterns
415631

416632
| Anti-Pattern | Correct Approach |
@@ -430,6 +646,8 @@ export 'src/widgets/app_text_field.dart';
430646
2. Compose Material widgets internally; read custom tokens via `context.appColors` / `context.appSpacing`
431647
3. Export the file from the barrel file (`lib/my_ui.dart`)
432648
4. Create `test/src/widgets/app_<name>_test.dart` with widget tests
649+
5. Add use cases in `widgetbook/lib/widgetbook/use_cases/app_<name>.dart` covering all variants
650+
6. Re-run `dart run build_runner build --delete-conflicting-outputs` in `widgetbook/`
433651

434652
### Adding a New Custom Token
435653

0 commit comments

Comments
 (0)