Loading...
Loading...
Enforces designed interaction feedback instead of the framework default — every committing interaction acknowledged in the same frame, motion that decorates state but never IS the state (the end state is identical with animations off), a named catalog of moments each declaring its trigger, duration role, haptic slot and reduced-motion fallback at design time, haptics as a HapticTheme event-to-intensity map fired exactly once on the commit frame (never per animation frame, never per coalesced input, never heavyImpact for an error) behind one central toggle, every animation interruptible so a tap resolves it to its end state, a bounded celebration that plays once and never loops or blocks input, an explicit stop condition on every repeating animation (off-route, backgrounded, reduced motion), and feedback on more than one channel so haptics and sound survive reduced motion. Use when adding or tuning an animation, a success or error state, a haptic, a celebration, or a screen transition.
npx skill4agent add zakariaf/flutter-skills motion-and-hapticsresolveMotionDuration.zerodesign-system-structurecustom-canvas-and-gesturesui-states-and-feedbackreferences/moment-catalog.mdresolveMotionDuration.zerodesign-system-structureHapticThemeHapticFeedbackselectionClicklightImpactmediumImpactlightImpactmediumImpactheavyImpactnullheavyImpactrepeat()accessibility-as-codei18n-rtl-l10n| Moment | Trigger | What changes | Duration role | Haptic | Reduced-motion fallback |
|---|---|---|---|---|---|
| Press | pointer down | press transform | shortest | | instant state, no transform |
| Reject | invalid input | bounded shake + message | short, interruptible | | message only, no shake |
| Commit | state committed | the transformation itself | medium | | end state appears instantly |
| Success | task complete | acknowledgment + result | long, skippable | | result appears already complete |
| Increment | counter advances | one emphasis pulse | short | | new value, no pulse |
| Transition | route change | the signature transition | medium | none | cross-fade or none |
design-system-structure// The event map lives in the theme; call sites name an event, never an intensity.
class HapticTheme extends ThemeExtension<HapticTheme> {
const HapticTheme({this.select, this.commit, this.reject, this.success});
/// A null slot is a DECLARED silence — the design said no haptic here.
final Future<void> Function()? select;
final Future<void> Function()? commit;
final Future<void> Function()? reject;
final Future<void> Function()? success;
// copyWith / lerp omitted — see design-system-structure for the ThemeExtension contract.
}
// One central gate; no call site remembers to check the setting.
void fire(WidgetRef ref, Future<void> Function()? slot) {
if (!ref.read(hapticsEnabledProvider)) return;
slot?.call();
}
// At the commit — once, on the frame the state actually changed (rules 6, 8).
void onCommit(BuildContext context, WidgetRef ref, EditIntent intent) {
ref.read(itemsProvider.notifier).apply(intent); // state first
fire(ref, Theme.of(context).extension<HapticTheme>()!.commit);
}HapticFeedback.*AnimationControlleronEndHapticFeedback.mediumImpact()onEndheavyImpactrepeat()HapticThemenulldesign-system-structureresolveMotioncustom-canvas-and-gesturesAnimationControllerrepaint:RepaintBoundaryui-states-and-feedbackSemanticsService.announceaccessibility-as-codeflutter-performancei18n-rtl-l10ndesign-review-workflowHapticFeedbackSystemSoundMediaQueryData.disableAnimations