diff --git a/docs/widgets/badge.mdx b/docs/widgets/badge.mdx new file mode 100644 index 00000000..91145f5a --- /dev/null +++ b/docs/widgets/badge.mdx @@ -0,0 +1,150 @@ +--- +title: "Badge" +description: "Documentation for Badge" +--- + +The Stac Badge allows you to build a Flutter Badge widget using JSON. +To know more about the Badge widget in Flutter, refer to the [official documentation](https://api.flutter.dev/flutter/material/Badge-class.html). + +## Properties + +| Property | Type | Description | +|------------------|---------------------------|------------------------------------------------------------------------------------------------| +| backgroundColor | `String?` | The badge's fill color (hex string). Defaults to `ColorScheme.error` from theme. | +| textColor | `String?` | The color of the badge's label text (hex string). Defaults to `ColorScheme.onError` from theme. | +| smallSize | `double?` | The diameter of the badge if [label] is null. Defaults to 6.0. | +| largeSize | `double?` | The badge's height if [label] is non-null. Defaults to 16.0. | +| textStyle | `StacTextStyle?` | The text style for the badge's label. | +| padding | `StacEdgeInsets?` | The padding added to the badge's label. Defaults to 4 pixels horizontal. | +| alignment | `StacAlignmentGeometry?` | Combined with [offset] to determine the location of the [label]. Defaults to `AlignmentDirectional.topEnd`. | +| offset | `StacOffset?` | Combined with [alignment] to determine the location of the [label]. | +| label | `Map?` | The badge's content, typically a [StacText] widget. If null, displays as a small filled circle. | +| count | `int?` | Convenience property for creating a badge with a numeric label. Automatically creates label showing count or '[maxCount]+' if count exceeds maxCount. | +| maxCount | `int?` | Maximum count value before showing '[maxCount]+' format. Only used when [count] is provided. Defaults to 999. | +| isLabelVisible | `bool?` | If false, the badge's [label] is not included. Defaults to `true`. | +| child | `Map?` | The widget that the badge is stacked on top of. Typically an Icon or IconButton. | + +## Example JSON + +### Basic Badge with Label + +```json +{ + "type": "badge", + "label": { + "type": "text", + "data": "5" + }, + "child": { + "type": "icon", + "icon": "notifications", + "size": 32 + }, + "backgroundColor": "#F44336", + "textColor": "#FFFFFF" +} +``` + +### Badge with Count + +```json +{ + "type": "badge", + "count": 5, + "child": { + "type": "icon", + "icon": "shopping_cart", + "size": 32 + } +} +``` + +### Badge with Count Exceeding MaxCount + +```json +{ + "type": "badge", + "count": 1000, + "maxCount": 99, + "child": { + "type": "icon", + "icon": "notifications", + "size": 32 + } +} +``` + +### Small Badge (No Label) + +```json +{ + "type": "badge", + "smallSize": 8, + "backgroundColor": "#F44336", + "child": { + "type": "icon", + "icon": "circle", + "size": 32 + } +} +``` + +### Badge on IconButton + +```json +{ + "type": "badge", + "count": 3, + "child": { + "type": "iconButton", + "icon": { + "type": "icon", + "icon": "notifications", + "size": 24 + }, + "padding": { + "left": 0, + "top": 0, + "right": 0, + "bottom": 0 + }, + "onPressed": { + "actionType": "none" + } + } +} +``` + +### Badge with Custom Styling + +```json +{ + "type": "badge", + "label": { + "type": "text", + "data": "NEW" + }, + "backgroundColor": "#4CAF50", + "textColor": "#FFFFFF", + "largeSize": 20, + "padding": { + "left": 8, + "top": 4, + "right": 8, + "bottom": 4 + }, + "alignment": { + "dx": 1.0, + "dy": -1.0 + }, + "offset": { + "dx": 4, + "dy": -4 + }, + "child": { + "type": "icon", + "icon": "mail", + "size": 32 + } +} +``` diff --git a/examples/stac_gallery/assets/json/badge_example.json b/examples/stac_gallery/assets/json/badge_example.json new file mode 100644 index 00000000..ec775d78 --- /dev/null +++ b/examples/stac_gallery/assets/json/badge_example.json @@ -0,0 +1,210 @@ +{ + "type": "scaffold", + "appBar": { + "type": "appBar", + "title": { + "type": "text", + "data": "Badge" + } + }, + "body": { + "type": "column", + "mainAxisAlignment": "center", + "crossAxisAlignment": "center", + "children": [ + { + "type": "text", + "data": "Badge with Label", + "style": { + "fontSize": 18, + "fontWeight": "bold" + } + }, + { + "type": "sizedBox", + "height": 16 + }, + { + "type": "row", + "mainAxisAlignment": "center", + "crossAxisAlignment": "center", + "children": [ + { + "type": "badge", + "label": { + "type": "text", + "data": "5" + }, + "child": { + "type": "icon", + "icon": "notifications", + "size": 32 + } + }, + { + "type": "sizedBox", + "width": 24 + }, + { + "type": "badge", + "label": { + "type": "text", + "data": "NEW" + }, + "backgroundColor": "#4CAF50", + "textColor": "#FFFFFF", + "child": { + "type": "icon", + "icon": "mail", + "size": 32 + } + } + ] + }, + { + "type": "sizedBox", + "height": 32 + }, + { + "type": "text", + "data": "Badge with Count", + "style": { + "fontSize": 18, + "fontWeight": "bold" + } + }, + { + "type": "sizedBox", + "height": 16 + }, + { + "type": "row", + "mainAxisAlignment": "center", + "crossAxisAlignment": "center", + "children": [ + { + "type": "badge", + "count": 5, + "child": { + "type": "icon", + "icon": "shopping_cart", + "size": 32 + } + }, + { + "type": "sizedBox", + "width": 24 + }, + { + "type": "badge", + "count": 99, + "maxCount": 99, + "child": { + "type": "icon", + "icon": "favorite", + "size": 32 + } + }, + { + "type": "sizedBox", + "width": 24 + }, + { + "type": "badge", + "count": 1000, + "maxCount": 99, + "child": { + "type": "icon", + "icon": "notifications", + "size": 32 + } + } + ] + }, + { + "type": "sizedBox", + "height": 32 + }, + { + "type": "text", + "data": "Small Badge (No Label)", + "style": { + "fontSize": 18, + "fontWeight": "bold" + } + }, + { + "type": "sizedBox", + "height": 16 + }, + { + "type": "row", + "mainAxisAlignment": "center", + "crossAxisAlignment": "center", + "children": [ + { + "type": "badge", + "smallSize": 8, + "backgroundColor": "#F44336", + "child": { + "type": "icon", + "icon": "circle", + "size": 32 + } + }, + { + "type": "sizedBox", + "width": 24 + }, + { + "type": "badge", + "smallSize": 12, + "backgroundColor": "#4CAF50", + "child": { + "type": "icon", + "icon": "check_circle", + "size": 32 + } + } + ] + }, + { + "type": "sizedBox", + "height": 32 + }, + { + "type": "text", + "data": "Badge on IconButton", + "style": { + "fontSize": 18, + "fontWeight": "bold" + } + }, + { + "type": "sizedBox", + "height": 16 + }, + { + "type": "badge", + "count": 3, + "child": { + "type": "iconButton", + "icon": { + "type": "icon", + "icon": "notifications", + "size": 24 + }, + "padding": { + "left": 0, + "top": 0, + "right": 0, + "bottom": 0 + }, + "onPressed": { + "actionType": "none" + } + } + } + ] + } +} diff --git a/examples/stac_gallery/assets/json/home_screen.json b/examples/stac_gallery/assets/json/home_screen.json index 1a7ef129..b15b0f57 100644 --- a/examples/stac_gallery/assets/json/home_screen.json +++ b/examples/stac_gallery/assets/json/home_screen.json @@ -22,6 +22,7 @@ } } }, + { "type": "listTile", "leading": { @@ -44,6 +45,28 @@ } } }, + { + "type": "listTile", + "leading": { + "type": "icon", + "icon": "badge" + }, + "title": { + "type": "text", + "data": "Stac Badge" + }, + "subtitle": { + "type": "text", + "data": "Display small status descriptors, counts, or notifications" + }, + "onTap": { + "actionType": "navigate", + "widgetJson": { + "type": "exampleScreen", + "assetPath": "assets/json/badge_example.json" + } + } + }, { "type": "listTile", "leading": { diff --git a/packages/stac/lib/src/framework/stac_service.dart b/packages/stac/lib/src/framework/stac_service.dart index 655f29e1..a235ad37 100644 --- a/packages/stac/lib/src/framework/stac_service.dart +++ b/packages/stac/lib/src/framework/stac_service.dart @@ -98,6 +98,7 @@ class StacService { const StacDefaultBottomNavigationControllerParser(), const StacWrapParser(), const StacAutoCompleteParser(), + const StacBadgeParser(), const StacTableParser(), const StacTableCellParser(), const StacCarouselViewParser(), diff --git a/packages/stac/lib/src/parsers/widgets/stac_badge/stac_badge_parser.dart b/packages/stac/lib/src/parsers/widgets/stac_badge/stac_badge_parser.dart new file mode 100644 index 00000000..90c0bd25 --- /dev/null +++ b/packages/stac/lib/src/parsers/widgets/stac_badge/stac_badge_parser.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stac/src/parsers/foundation/foundation.dart'; +import 'package:stac/stac.dart'; +import 'package:stac_core/stac_core.dart'; + +class StacBadgeParser extends StacParser { + const StacBadgeParser(); + + @override + String get type => WidgetType.badge.name; + + @override + StacBadge getModel(Map json) => StacBadge.fromJson(json); + + @override + Widget parse(BuildContext context, StacBadge model) { + // Handle count property (Badge.count equivalent) + Widget? label; + if (model.count != null) { + // Validate count and maxCount (matching Flutter's assertions) + assert(model.count! >= 0, 'count must be non-negative'); + final maxCount = model.maxCount ?? 999; + assert(maxCount > 0, 'maxCount must be positive'); + + // Create label from count (matching Flutter's Badge.count logic) + final labelText = model.count! > maxCount + ? '$maxCount+' + : '${model.count}'; + label = Text(labelText); + } else { + // Use explicit label if count is not provided + label = model.label?.parse(context); + } + + return Badge( + backgroundColor: model.backgroundColor?.toColor(context), + textColor: model.textColor?.toColor(context), + smallSize: model.smallSize, + largeSize: model.largeSize, + textStyle: model.textStyle?.parse(context), + padding: model.padding?.parse, + alignment: model.alignment?.parse, + offset: model.offset?.parse, + label: label, + isLabelVisible: model.isLabelVisible ?? true, + child: model.child?.parse(context), + ); + } +} diff --git a/packages/stac/lib/src/parsers/widgets/widgets.dart b/packages/stac/lib/src/parsers/widgets/widgets.dart index 02879d0d..cf6ea45b 100644 --- a/packages/stac/lib/src/parsers/widgets/widgets.dart +++ b/packages/stac/lib/src/parsers/widgets/widgets.dart @@ -3,6 +3,7 @@ export 'package:stac/src/parsers/widgets/stac_align/stac_align_parser.dart'; export 'package:stac/src/parsers/widgets/stac_aspect_ratio/stac_aspect_ratio_parser.dart'; export 'package:stac/src/parsers/widgets/stac_auto_complete/stac_auto_complete_parser.dart'; export 'package:stac/src/parsers/widgets/stac_backdrop_filter/stac_backdrop_filter_parser.dart'; +export 'package:stac/src/parsers/widgets/stac_badge/stac_badge_parser.dart'; export 'package:stac/src/parsers/widgets/stac_bottom_navigation_bar/stac_bottom_navigation_bar_parser.dart'; export 'package:stac/src/parsers/widgets/stac_bottom_navigation_view/stac_bottom_navigation_view_parser.dart'; export 'package:stac/src/parsers/widgets/stac_card/stac_card_parser.dart'; diff --git a/packages/stac_core/lib/foundation/specifications/widget_type.dart b/packages/stac_core/lib/foundation/specifications/widget_type.dart index 00b3bb38..ffae0443 100644 --- a/packages/stac_core/lib/foundation/specifications/widget_type.dart +++ b/packages/stac_core/lib/foundation/specifications/widget_type.dart @@ -21,6 +21,9 @@ enum WidgetType { /// Backdrop filter widget backdropFilter, + /// Badge widget + badge, + /// Bottom navigation bar widget bottomNavigationBar, diff --git a/packages/stac_core/lib/widgets/badge/stac_badge.dart b/packages/stac_core/lib/widgets/badge/stac_badge.dart new file mode 100644 index 00000000..d8b1c4df --- /dev/null +++ b/packages/stac_core/lib/widgets/badge/stac_badge.dart @@ -0,0 +1,218 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stac_core/core/stac_widget.dart'; +import 'package:stac_core/foundation/foundation.dart'; + +part 'stac_badge.g.dart'; + +/// A Stac model representing Flutter's [Badge] widget. +/// +/// A badge's [label] conveys a small amount of information about its +/// [child], like a count or status. If the label is null then this is +/// a "small" badge that's displayed as a [smallSize] diameter filled +/// circle. Otherwise this is a StadiumBorder shaped "large" badge +/// with height [largeSize]. +/// +/// Badges are typically used to decorate the icon within a +/// [BottomNavigationBarItem] or a [NavigationRailDestination] +/// or a button's icon. +/// +/// {@tool snippet} +/// Dart Example: +/// ```dart +/// // Badge with explicit label +/// StacBadge( +/// label: StacText(data: '5'), +/// child: StacIcon(icon: 'notifications'), +/// backgroundColor: StacColors.red, +/// textColor: StacColors.white, +/// ) +/// +/// // Badge with count (convenience) +/// StacBadge( +/// count: 5, +/// child: StacIcon(icon: 'notifications'), +/// ) +/// +/// // Badge with count exceeding maxCount +/// StacBadge( +/// count: 1000, +/// maxCount: 99, +/// child: StacIcon(icon: 'notifications'), +/// ) // Will display "99+" +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// JSON Example: +/// ```json +/// { +/// "type": "badge", +/// "label": { "type": "text", "data": "5" }, +/// "child": { "type": "icon", "icon": "notifications" }, +/// "backgroundColor": "#F44336", +/// "textColor": "#FFFFFF" +/// } +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// JSON Example with count: +/// ```json +/// { +/// "type": "badge", +/// "count": 5, +/// "child": { "type": "icon", "icon": "notifications" } +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// * Flutter's Badge documentation (`https://api.flutter.dev/flutter/material/Badge-class.html`) +@JsonSerializable(explicitToJson: true) +class StacBadge extends StacWidget { + /// Creates a [StacBadge] that stacks [label] on top of [child]. + /// + /// If [label] is null then just a filled circle is displayed. Otherwise + /// the [label] is displayed within a StadiumBorder shaped area. + /// + /// If [count] is provided, it will automatically create a label showing + /// the count value or '[maxCount]+' if count exceeds [maxCount]. + /// When [count] is provided, any explicit [label] will be ignored. + const StacBadge({ + this.backgroundColor, + this.textColor, + this.smallSize, + this.largeSize, + this.textStyle, + this.padding, + this.alignment, + this.offset, + this.label, + this.count, + this.maxCount = 999, + this.isLabelVisible = true, + this.child, + }); + + /// Convenience constructor for creating a badge with a numeric label based on [count]. + /// + /// Initializes [count] with the provided value and automatically creates a label + /// showing the count value or '[maxCount]+' if count exceeds [maxCount]. + /// + /// For example, if [count] is 1000 and [maxCount] is 99, the label will display '99+'. + /// + /// The [count] must be non-negative (>= 0) and [maxCount] must be positive (> 0). + /// + /// {@tool snippet} + /// Dart Example: + /// ```dart + /// StacBadge.count( + /// count: 5, + /// child: StacIcon(icon: 'notifications'), + /// ) + /// + /// StacBadge.count( + /// count: 1000, + /// maxCount: 99, + /// child: StacIcon(icon: 'notifications'), + /// ) // Will display "99+" + /// ``` + /// {@end-tool} + factory StacBadge.count({ + String? backgroundColor, + String? textColor, + double? smallSize, + double? largeSize, + StacTextStyle? textStyle, + StacEdgeInsets? padding, + StacAlignmentGeometry? alignment, + StacOffset? offset, + required int count, + int maxCount = 999, + bool isLabelVisible = true, + StacWidget? child, + }) { + assert(count >= 0, 'count must be non-negative'); + assert(maxCount > 0, 'maxCount must be positive'); + return StacBadge( + backgroundColor: backgroundColor, + textColor: textColor, + smallSize: smallSize, + largeSize: largeSize, + textStyle: textStyle, + padding: padding, + alignment: alignment, + offset: offset, + count: count, + maxCount: maxCount, + isLabelVisible: isLabelVisible, + child: child, + ); + } + + /// The badge's fill color (hex string). + final String? backgroundColor; + + /// The color of the badge's label text (hex string). + final String? textColor; + + /// The diameter of the badge if [label] is null. + final double? smallSize; + + /// The badge's height if [label] is non-null. + final double? largeSize; + + /// The text style for the badge's label. + final StacTextStyle? textStyle; + + /// The padding added to the badge's label. + final StacEdgeInsets? padding; + + /// Combined with [offset] to determine the location of the [label]. + final StacAlignmentGeometry? alignment; + + /// Combined with [alignment] to determine the location of the [label]. + final StacOffset? offset; + + /// The badge's content, typically a [StacText] widget. + /// + /// If [count] is provided, this will be ignored and a label will be + /// automatically generated from the count. + final StacWidget? label; + + /// Convenience property for creating a badge with a numeric label. + /// + /// If provided, automatically creates a label showing: + /// - the [count] value if it is less than or equal to [maxCount], + /// - otherwise, shows '[maxCount]+'. + /// + /// For example, if [count] is 1000 and [maxCount] is 999, the label + /// will display '999+'. + /// + /// When [count] is provided, any explicit [label] will be ignored. + /// The [count] must be non-negative (>= 0). + final int? count; + + /// Maximum count value before showing '[maxCount]+' format. + /// + /// Only used when [count] is provided. Defaults to 999. + /// Must be positive (> 0). + final int? maxCount; + + /// If false, the badge's [label] is not included. + final bool? isLabelVisible; + + /// The widget that the badge is stacked on top of. + final StacWidget? child; + + @override + String get type => WidgetType.badge.name; + + /// Creates a [StacBadge] from a JSON map. + factory StacBadge.fromJson(Map json) => + _$StacBadgeFromJson(json); + + /// Converts this [StacBadge] to JSON. + @override + Map toJson() => _$StacBadgeToJson(this); +} diff --git a/packages/stac_core/lib/widgets/badge/stac_badge.g.dart b/packages/stac_core/lib/widgets/badge/stac_badge.g.dart new file mode 100644 index 00000000..66cf27c7 --- /dev/null +++ b/packages/stac_core/lib/widgets/badge/stac_badge.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stac_badge.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StacBadge _$StacBadgeFromJson(Map json) => StacBadge( + backgroundColor: json['backgroundColor'] as String?, + textColor: json['textColor'] as String?, + smallSize: (json['smallSize'] as num?)?.toDouble(), + largeSize: (json['largeSize'] as num?)?.toDouble(), + textStyle: json['textStyle'] == null + ? null + : StacTextStyle.fromJson(json['textStyle']), + padding: json['padding'] == null + ? null + : StacEdgeInsets.fromJson(json['padding']), + alignment: json['alignment'] == null + ? null + : StacAlignmentGeometry.fromJson( + json['alignment'] as Map, + ), + offset: json['offset'] == null + ? null + : StacOffset.fromJson(json['offset'] as Map), + label: json['label'] == null + ? null + : StacWidget.fromJson(json['label'] as Map), + count: (json['count'] as num?)?.toInt(), + maxCount: (json['maxCount'] as num?)?.toInt() ?? 999, + isLabelVisible: json['isLabelVisible'] as bool? ?? true, + child: json['child'] == null + ? null + : StacWidget.fromJson(json['child'] as Map), +); + +Map _$StacBadgeToJson(StacBadge instance) => { + 'backgroundColor': instance.backgroundColor, + 'textColor': instance.textColor, + 'smallSize': instance.smallSize, + 'largeSize': instance.largeSize, + 'textStyle': instance.textStyle?.toJson(), + 'padding': instance.padding?.toJson(), + 'alignment': instance.alignment?.toJson(), + 'offset': instance.offset?.toJson(), + 'label': instance.label?.toJson(), + 'count': instance.count, + 'maxCount': instance.maxCount, + 'isLabelVisible': instance.isLabelVisible, + 'child': instance.child?.toJson(), + 'type': instance.type, +}; diff --git a/packages/stac_core/lib/widgets/widgets.dart b/packages/stac_core/lib/widgets/widgets.dart index 9d54fa62..81e71c45 100644 --- a/packages/stac_core/lib/widgets/widgets.dart +++ b/packages/stac_core/lib/widgets/widgets.dart @@ -6,6 +6,7 @@ export 'app_bar/stac_app_bar.dart'; export 'aspect_ratio/stac_aspect_ratio.dart'; export 'auto_complete/stac_auto_complete.dart'; export 'backdrop_filter/stac_backdrop_filter.dart'; +export 'badge/stac_badge.dart'; export 'bottom_navigation_bar/stac_bottom_navigation_bar.dart'; export 'bottom_navigation_view/stac_bottom_navigation_view.dart'; export 'card/stac_card.dart';