Add Widgetbook setup with theme configuration, analysis options, and initial component use cases

This commit is contained in:
Emily Pauli 2025-10-25 22:59:48 +02:00
commit 80ca8f665a
46 changed files with 6031 additions and 2 deletions

View file

@ -0,0 +1,27 @@
# INDEX — slider_m3e
This index lists all Widgetbook use cases generated for the slider_m3e package.
## Components and Variants
- SliderM3E
- default
- discrete
- disabled
- extremes_0_to_100
- negative_range
- min_equals_max
- RangeSliderM3E
- default
- discrete
- disabled
- extremes_0_to_100
- negative_range
- min_equals_max
Notes
- All variants follow plan/guide.md rules with @UseCase annotations and required method signatures.
- Knobs are provided for critical parameters (values, min, max, divisions) and key visuals (size, emphasis, shapeFamily, density, value indicator).
- Callbacks print helpful messages to the console.
- Range labels are included where useful; complex configurations are kept simple with TODOs where appropriate.

View file

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:slider_m3e/slider_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
Widget _buildRangeSliderM3EDemo(
BuildContext context, {
double? forcedMin,
double? forcedMax,
int? forcedDivisions,
bool disabled = false,
bool zeroRange = false,
}) {
// Critical params via knobs
final minKnob = forcedMin ??
context.knobs.double.slider(
label: 'min',
initialValue: 0,
min: -100,
max: 100,
divisions: 40,
);
final maxKnob = forcedMax ??
context.knobs.double.slider(
label: 'max',
initialValue: 100,
min: -100,
max: 200,
divisions: 60,
);
double min = minKnob;
double max = maxKnob;
// Force min == max when zeroRange is requested
if (zeroRange) {
final fixed = context.knobs.double.slider(
label: 'fixed value (min==max)',
initialValue: 25,
min: -100,
max: 100,
divisions: 40,
);
min = fixed;
max = fixed;
}
if (min >= max) {
// Ensure valid range
max = min + 1;
}
// Pick start/end values within [min, max]
final start = context.knobs.double.slider(
label: 'start',
initialValue: min + (max - min) * 0.25,
min: min,
max: max,
divisions: 100,
);
final end = context.knobs.double.slider(
label: 'end',
initialValue: min + (max - min) * 0.75,
min: min,
max: max,
divisions: 100,
);
final divisions = forcedDivisions ??
context.knobs.intOrNull.slider(
label: 'divisions',
initialValue: null,
min: 1,
max: 20,
divisions: 19,
);
// Visual params via knobs
final size = context.knobs.object.dropdown<SliderM3ESize>(
label: 'size',
initialOption: SliderM3ESize.medium,
options: SliderM3ESize.values,
labelBuilder: (v) => v.name,
);
final emphasis = context.knobs.object.dropdown<SliderM3EEmphasis>(
label: 'emphasis',
initialOption: SliderM3EEmphasis.primary,
options: SliderM3EEmphasis.values,
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<SliderM3EShapeFamily>(
label: 'shapeFamily',
initialOption: SliderM3EShapeFamily.round,
options: SliderM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<SliderM3EDensity>(
label: 'density',
initialOption: SliderM3EDensity.regular,
options: SliderM3EDensity.values,
labelBuilder: (v) => v.name,
);
final showValueIndicator = context.knobs.boolean(
label: 'showValueIndicator',
initialValue: false,
);
// Labels & semantics
final hasLabels = context.knobs.boolean(label: 'use RangeLabels', initialValue: false);
final startLabel = hasLabels
? context.knobs.string(label: 'start label', initialValue: 'Start')
: null;
final endLabel = hasLabels
? context.knobs.string(label: 'end label', initialValue: 'End')
: null;
final labels = hasLabels && startLabel != null && endLabel != null
? RangeLabels(startLabel, endLabel)
: null;
final semanticLabel = zeroRange
? null
: context.knobs.stringOrNull(
label: 'semanticLabel',
initialValue: 'Progress',
);
final onChanged = disabled
? null
: (RangeValues v) => print('RangeSliderM3E onChanged -> ${v.start} - ${v.end}');
final onChangeStart = disabled
? null
: (RangeValues v) => print('RangeSliderM3E onChangeStart -> ${v.start} - ${v.end}');
final onChangeEnd = disabled
? null
: (RangeValues v) => print('RangeSliderM3E onChangeEnd -> ${v.start} - ${v.end}');
return Padding(
padding: const EdgeInsets.all(16),
child: RangeSliderM3E(
values: RangeValues(
start.clamp(min, max),
end.clamp(min, max),
),
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
min: min,
max: max,
divisions: divisions,
labels: labels,
semanticLabel: semanticLabel,
size: size,
emphasis: emphasis,
shapeFamily: shape,
density: density,
showValueIndicator: showValueIndicator,
),
);
}
@UseCase(name: 'default', type: RangeSliderM3E)
Widget buildRangeSliderM3EDefaultUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context);
}
@UseCase(name: 'discrete', type: RangeSliderM3E)
Widget buildRangeSliderM3EDiscreteUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, forcedDivisions: 6);
}
@UseCase(name: 'disabled', type: RangeSliderM3E)
Widget buildRangeSliderM3EDisabledUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, disabled: true);
}
@UseCase(name: 'extremes_0_to_100', type: RangeSliderM3E)
Widget buildRangeSliderM3EExtremes0100UseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, forcedMin: 0, forcedMax: 100);
}
@UseCase(name: 'negative_range', type: RangeSliderM3E)
Widget buildRangeSliderM3ENegativeRangeUseCase(BuildContext context) {
return _buildRangeSliderM3EDemo(context, forcedMin: -100, forcedMax: 0);
}
@UseCase(name: 'min_equals_max', type: RangeSliderM3E)
Widget buildRangeSliderM3EMinEqualsMaxUseCase(BuildContext context) {
// Zero-range; interactions disabled to avoid semantic division by zero
return _buildRangeSliderM3EDemo(context, zeroRange: true, disabled: true);
}

View file

@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import 'package:slider_m3e/slider_m3e.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
Widget _buildSliderM3EDemo(
BuildContext context, {
double? forcedMin,
double? forcedMax,
int? forcedDivisions,
bool disabled = false,
bool zeroRange = false,
}) {
// Critical params via knobs
final minKnob = forcedMin ??
context.knobs.double.slider(
label: 'min',
initialValue: 0,
min: -100,
max: 100,
divisions: 40,
);
final maxKnob = forcedMax ??
context.knobs.double.slider(
label: 'max',
initialValue: 100,
min: -100,
max: 200,
divisions: 60,
);
double min = minKnob;
double max = maxKnob;
// Special handling when zeroRange is requested
if (zeroRange) {
// Force min == max and disable interactions to avoid semantic division by zero
final fixed = context.knobs.double.slider(
label: 'fixed value (min==max)',
initialValue: 50,
min: -100,
max: 100,
divisions: 40,
);
min = fixed;
max = fixed;
}
if (min >= max) {
// Guard against invalid ranges; ensure there is at least a small span
max = min + 1;
}
final value = context.knobs.double.slider(
label: 'value',
initialValue: (min + max) / 2,
min: min,
max: max,
divisions: 100,
);
final divisions = forcedDivisions ??
context.knobs.intOrNull.slider(
label: 'divisions',
initialValue: null,
min: 1,
max: 20,
divisions: 19,
);
// Visual params via knobs
final size = context.knobs.object.dropdown<SliderM3ESize>(
label: 'size',
initialOption: SliderM3ESize.medium,
options: SliderM3ESize.values,
labelBuilder: (v) => v.name,
);
final emphasis = context.knobs.object.dropdown<SliderM3EEmphasis>(
label: 'emphasis',
initialOption: SliderM3EEmphasis.primary,
options: SliderM3EEmphasis.values,
labelBuilder: (v) => v.name,
);
final shape = context.knobs.object.dropdown<SliderM3EShapeFamily>(
label: 'shapeFamily',
initialOption: SliderM3EShapeFamily.round,
options: SliderM3EShapeFamily.values,
labelBuilder: (v) => v.name,
);
final density = context.knobs.object.dropdown<SliderM3EDensity>(
label: 'density',
initialOption: SliderM3EDensity.regular,
options: SliderM3EDensity.values,
labelBuilder: (v) => v.name,
);
final showValueIndicator = context.knobs.boolean(
label: 'showValueIndicator',
initialValue: false,
);
final withStartIcon = context.knobs.boolean(
label: 'startIcon',
initialValue: false,
);
final withEndIcon = context.knobs.boolean(
label: 'endIcon',
initialValue: false,
);
// Content params
final label = context.knobs.stringOrNull(
label: 'label',
initialValue: null,
);
// Avoid semantic callback when zeroRange to prevent divide-by-zero in tokens formatting
final semanticLabel = zeroRange
? null
: context.knobs.stringOrNull(
label: 'semanticLabel',
initialValue: 'Progress',
);
final onChanged = disabled
? null
: (double v) => print('SliderM3E onChanged -> $v');
final onChangeStart = disabled
? null
: (double v) => print('SliderM3E onChangeStart -> $v');
final onChangeEnd = disabled
? null
: (double v) => print('SliderM3E onChangeEnd -> $v');
return Padding(
padding: const EdgeInsets.all(16),
child: SliderM3E(
value: value,
onChanged: onChanged,
onChangeStart: onChangeStart,
onChangeEnd: onChangeEnd,
min: min,
max: max,
divisions: divisions,
label: label,
semanticLabel: semanticLabel,
size: size,
emphasis: emphasis,
shapeFamily: shape,
density: density,
showValueIndicator: showValueIndicator,
startIcon: withStartIcon ? const Icon(Icons.remove) : null,
endIcon: withEndIcon ? const Icon(Icons.add) : null,
),
);
}
@UseCase(name: 'default', type: SliderM3E)
Widget buildSliderM3EDefaultUseCase(BuildContext context) {
return _buildSliderM3EDemo(context);
}
@UseCase(name: 'discrete', type: SliderM3E)
Widget buildSliderM3EDiscreteUseCase(BuildContext context) {
// Provide a default divisions count; still knob-customizable via 'divisions'
return _buildSliderM3EDemo(context, forcedDivisions: 5);
}
@UseCase(name: 'disabled', type: SliderM3E)
Widget buildSliderM3EDisabledUseCase(BuildContext context) {
return _buildSliderM3EDemo(context, disabled: true);
}
@UseCase(name: 'extremes_0_to_100', type: SliderM3E)
Widget buildSliderM3EExtremes0100UseCase(BuildContext context) {
return _buildSliderM3EDemo(context, forcedMin: 0, forcedMax: 100);
}
@UseCase(name: 'negative_range', type: SliderM3E)
Widget buildSliderM3ENegativeRangeUseCase(BuildContext context) {
return _buildSliderM3EDemo(context, forcedMin: -100, forcedMax: 0);
}
@UseCase(name: 'min_equals_max', type: SliderM3E)
Widget buildSliderM3EMinEqualsMaxUseCase(BuildContext context) {
// Show a zero-range slider (min == max), interactions disabled to avoid semantic math
return _buildSliderM3EDemo(context, zeroRange: true, disabled: true);
}