diff --git a/example/lib/home.dart b/example/lib/home.dart index 6e555557..63c0754a 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -187,10 +187,14 @@ class _HomePageState extends State { title: "GS Navigation Rail", routePath: "/navigation-rail-preview", ), - const NavButton( + const NavButton( title: "GS Slider", routePath: "/slider-example", ), + const NavButton( + title: "GS Select", + routePath: "/select-example", + ), // // ===== Internal Testing Widgets ===== // const NavButton( diff --git a/example/lib/routes/router.dart b/example/lib/routes/router.dart index b8608de1..fa820494 100644 --- a/example/lib/routes/router.dart +++ b/example/lib/routes/router.dart @@ -3,6 +3,7 @@ import 'package:gluestack_ui_example/home.dart'; import 'package:gluestack_ui_example/widgets/components/widgets/accordian_example.dart'; import 'package:gluestack_ui_example/widgets/components/widgets/bottom_sheet_example.dart'; import 'package:gluestack_ui_example/widgets/components/widgets/navigation_rail_example.dart'; +import 'package:gluestack_ui_example/widgets/components/widgets/select_example.dart'; import 'package:gluestack_ui_example/widgets/components/widgets/slider_example.dart'; import 'package:gluestack_ui_example/widgets/storybook/storybook.dart'; import 'package:gluestack_ui_example/widgets/storybook/storybook_wrapper.dart'; @@ -198,6 +199,11 @@ final GoRouter router = GoRouter( path: "slider-example", builder: (context, state) => const SliderExample(), ), + GoRoute( + path: "select-example", + builder: (context, state) => const SelectExample(), + ), + // Generate individual Storybook screens for every widget. This is referenced in docs website iframe. ...kStories.map( diff --git a/example/lib/widgets/components/widgets/select_example.dart b/example/lib/widgets/components/widgets/select_example.dart new file mode 100644 index 00000000..07a385bb --- /dev/null +++ b/example/lib/widgets/components/widgets/select_example.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:gluestack_ui/gluestack_ui.dart'; +import 'package:gluestack_ui_example/widgets/components/layout/base_layout.dart'; +import 'package:gluestack_ui_example/widgets/components/layout/custom_gs_layout.dart'; +import 'package:gluestack_ui_example/widgets/components/layout/drop_down.dart'; + +class SelectExample extends StatefulWidget { + const SelectExample({super.key}); + + @override + State createState() => _SelectExampleState(); +} + +class _SelectExampleState extends State { + final List dropdownSizeOptions = [ + GSSelectSizes.$sm, + GSSelectSizes.$md, + GSSelectSizes.$lg, + GSSelectSizes.$xl, + ]; + GSSelectSizes selectedSizeOption = GSSelectSizes.$md; + + final List dropdownVariantOptions = [ + GSSelectVariants.outline, + GSSelectVariants.rounded, + GSSelectVariants.underlined, + ]; + GSSelectVariants selectedVariantOption = GSSelectVariants.outline; + + void updateSizeSelectedOption(dynamic newOption) { + setState(() { + selectedSizeOption = newOption; + }); + } + + void updateVariantSelectedOption(dynamic newOption) { + setState(() { + selectedVariantOption = newOption; + }); + } + + @override + Widget build(BuildContext context) { + var code = ''' +GSSelect( + content: GSSelectContent( + style: GSStyle( + width: 300, + height: 200, + padding: const EdgeInsets.symmetric(horizontal: 15), + ), + options: const [ + 'UX Research', + 'Web Development', + 'Cross Platform Development Process', + 'UI Designing', + 'Backend Development' + ], + disabledOptions: const ['UI Designing', 'Backend Development'], + ), + hintText: const GSSelectHeaderText("Select option"), + style: GSStyle( + width: 300, + ), + icon: const GSSelectIcon( + iconData: Icons.arrow_drop_down_outlined, + ), + size: $selectedSizeOption, + variant: $selectedVariantOption, +), + '''; + + return CustomGSLayout( + title: "Select", + style: GSStyle( + dark: GSStyle(bg: $GSColors.black), + ), + body: BaseLayout( + code: code, + component: GSSelect( + onClose: () { + setState(() {}); + }, + onOpen: () {}, + onValueChange: (value) {}, + content: GSSelectContent( + style: GSStyle( + width: 300, + height: 200, + padding: const EdgeInsets.symmetric(horizontal: 15), + ), + options: const [ + 'UX Research', + 'Web Development', + 'Cross Platform Development Process', + 'UI Designing', + 'Backend Development' + ], + disabledOptions: const ['UI Designing', 'Backend Development'], + ), + label: const GSSelectHeaderText("Select option"), + style: GSStyle( + width: 300, + ), + icon: const GSSelectIcon( + iconData: Icons.arrow_drop_down_outlined, + ), + size: selectedSizeOption, + variant: selectedVariantOption, + ), + controls: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomDropDown( + title: "size", + dropdownOptions: dropdownSizeOptions, + selectedOption: selectedSizeOption, + onChanged: updateSizeSelectedOption, + ), + const SizedBox(height: 20), + CustomDropDown( + title: "variant", + dropdownOptions: dropdownVariantOptions, + selectedOption: selectedVariantOption, + onChanged: updateVariantSelectedOption, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/storybook/widgets/public.dart b/example/lib/widgets/storybook/widgets/public.dart index db3f37b5..4c91677f 100644 --- a/example/lib/widgets/storybook/widgets/public.dart +++ b/example/lib/widgets/storybook/widgets/public.dart @@ -1,4 +1,5 @@ import 'package:gluestack_ui_example/widgets/storybook/widgets/header_story.dart'; +import 'package:gluestack_ui_example/widgets/storybook/widgets/select_story.dart'; import 'package:gluestack_ui_example/widgets/storybook/widgets/slider_story.dart'; import 'package:gluestack_ui_example/widgets/storybook/widgets/tab_story.dart'; import 'alert_dialog_story.dart'; @@ -59,6 +60,7 @@ final List kStories = [ ProgressStory(), RadioButtonStory(), ScrollStory(), + SelectStory(), SliderStory(), SpinnerStory(), StackStory(), diff --git a/example/lib/widgets/storybook/widgets/select_story.dart b/example/lib/widgets/storybook/widgets/select_story.dart new file mode 100644 index 00000000..3c75b6da --- /dev/null +++ b/example/lib/widgets/storybook/widgets/select_story.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:gluestack_ui/gluestack_ui.dart'; +import 'base_story_widget.dart'; +import 'package:storybook_flutter/storybook_flutter.dart'; + +final List> variantOptions = + generateEnumOptions(GSSelectVariants.values); + +final List> sizeOptions = generateEnumOptions(GSSelectSizes.values); + +final class SelectStory extends StoryWidget { + @override + Story createStoryWidget() { + return Story( + name: storyName, + builder: (context) => GSSelect( + style: GSStyle( + width: 300, padding: const EdgeInsets.symmetric(horizontal: 15)), + content: GSSelectContent( + style: GSStyle( + borderRadius: 6, + width: 300, + height: 200, + padding: const EdgeInsets.all(5)), + options: const [ + 'UX Research', + 'Web Development', + 'Cross Platform Development Process', + 'UI Designing', + 'Backend Development', + ], + ), + label: const GSSelectHeaderText("Select option"), + icon: const GSSelectIcon( + iconData: Icons.arrow_drop_down_outlined, + ), + variant: GSSelectVariants.values[context.knobs + .options(label: 'Variant', initial: 0, options: variantOptions)], + size: GSSelectSizes.values[context.knobs + .options(label: 'Size', initial: 0, options: sizeOptions)], + ), + ); + } + + @override + String get routePath => "select-preview"; + + @override + String get storyName => "Select"; +} diff --git a/lib/gluestack_ui.dart b/lib/gluestack_ui.dart index edb7233b..f0c524c5 100644 --- a/lib/gluestack_ui.dart +++ b/lib/gluestack_ui.dart @@ -47,3 +47,4 @@ export 'src/widgets/gs_tabs/public.dart'; export 'src/widgets/gs_layout/public.dart'; export 'src/widgets/gs_header/public.dart'; export 'src/widgets/gs_slider/public.dart'; +export 'src/widgets/gs_select/public.dart'; diff --git a/lib/src/provider/gluestack_provider.dart b/lib/src/provider/gluestack_provider.dart index 772523d6..25a458be 100644 --- a/lib/src/provider/gluestack_provider.dart +++ b/lib/src/provider/gluestack_provider.dart @@ -1,3 +1,12 @@ +import 'package:gluestack_ui/src/theme/config/select/select.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_content.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_icon.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_item.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_selected_input.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_selection_header_text.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_text.dart'; +import 'package:gluestack_ui/src/theme/config/select/select_trigger.dart'; + import 'provider.dart'; final getIt = GetIt.instance; @@ -146,6 +155,15 @@ class GluestackCustomConfig { Map? sliderThumb; Map? sliderFilledTrack; + Map? select; + Map? selectTrigger; + Map? selectText; + Map? selectSelectionHeaderText; + Map? selectSelectedInput; + Map? selectIcon; + Map? selectContent; + Map? selectItem; + //GS Layout Map? layout; @@ -156,6 +174,17 @@ class GluestackCustomConfig { this.sliderTrack, this.sliderThumb, this.sliderFilledTrack, + + // select + + this.select, + this.selectContent, + this.selectItem, + this.selectIcon, + this.selectSelectionHeaderText, + this.selectSelectedInput, + this.selectText, + this.selectTrigger, //tabs this.tabs, this.tabsTab, @@ -316,6 +345,18 @@ class GluestackCustomConfig { sliderThumb = mergeConfigs(sliderThumbData, sliderThumb); sliderFilledTrack = mergeConfigs(sliderFilledTrackData, sliderFilledTrack); + // select + select = mergeConfigs(selectData, select); + selectIcon = mergeConfigs(selectIconData, selectIcon); + selectItem = mergeConfigs(selectItemData, selectItem); + selectContent = mergeConfigs(selectContentData, selectContent); + selectSelectedInput = + mergeConfigs(selectSelectedInputData, selectSelectedInput); + selectTrigger = mergeConfigs(selectTriggerData, selectTrigger); + selectText = mergeConfigs(selectTextData, selectText); + selectSelectionHeaderText = + mergeConfigs(selectSelectionHeaderTextData, selectSelectionHeaderText); + //tabs tabs = mergeConfigs(tabsData, tabs); tabsTab = mergeConfigs(tabsTabData, tabsTab); diff --git a/lib/src/style/gs_config_style_internal.dart b/lib/src/style/gs_config_style_internal.dart index 513fbd51..6e2d3abd 100644 --- a/lib/src/style/gs_config_style_internal.dart +++ b/lib/src/style/gs_config_style_internal.dart @@ -357,6 +357,9 @@ class GSConfigStyle extends BaseStyle { double? trackWidth; double? thumbHeight; double? thumbWidth; + double? borderTopLeftRadius; + double? borderTopRightRadius; + GSCursors? cursors; GSPlacement? placement; @@ -437,6 +440,8 @@ class GSConfigStyle extends BaseStyle { this.trackHeight, this.trackWidth, this.thumbHeight, + this.borderTopLeftRadius, + this.borderTopRightRadius, this.thumbWidth, this.outlineColor, this.cursors, @@ -591,6 +596,8 @@ class GSConfigStyle extends BaseStyle { trackWidth: overrideStyle?.trackWidth ?? trackWidth, thumbHeight: overrideStyle?.thumbHeight ?? thumbHeight, thumbWidth: overrideStyle?.thumbWidth ?? thumbWidth, + borderTopLeftRadius: overrideStyle?.borderTopLeftRadius ?? borderTopLeftRadius, + borderTopRightRadius: overrideStyle?.borderTopRightRadius ?? borderTopRightRadius, cursors: overrideStyle?.cursors ?? cursors, isVisible: overrideStyle?.isVisible ?? isVisible, direction: overrideStyle?.direction ?? direction, @@ -647,6 +654,8 @@ class GSConfigStyle extends BaseStyle { trackWidth: gsStyle.trackWidth, thumbHeight: gsStyle.thumbHeight, thumbWidth: gsStyle.thumbWidth, + borderTopLeftRadius: gsStyle.borderTopLeftRadius, + borderTopRightRadius: gsStyle.borderTopRightRadius, cursors: gsStyle.cursors, placement: gsStyle.placement, isVisible: gsStyle.isVisible, @@ -720,8 +729,8 @@ class GSConfigStyle extends BaseStyle { flexDirection: resolveFlexDirectionFromString(data?['flexDirection']), height: data?['h'] is int ? double.parse('${data?['h']}.0') - : resolveSpaceFromString( - data?['h'].toString() ?? data?['height'].toString(), + : resolveSpaceFromString(( + data?['h'] ?? data?['height']).toString(), ), width: data?['w'] != null ? data!['w']?.contains('100%') @@ -826,7 +835,8 @@ class GSConfigStyle extends BaseStyle { bg: resolveColorTokenFromString( data?['bg'] ?? data?['_item']?['backgroundColor'] ?? - data?['backgroundColor'], + data?['backgroundColor'] ?? + data?['_sectionHeaderBackground'], ), borderWidth: data?['borderWidth'] != null ? double.tryParse(data!['borderWidth']!.toString()) ?? @@ -998,7 +1008,7 @@ class GSConfigStyle extends BaseStyle { ), onDisabled: GSConfigStyle( opacity: data?[':disabled']?['opacity'], - bg : resolveColorTokenFromString(data?[':disabled']?['bg']), + bg: resolveColorTokenFromString(data?[':disabled']?['bg']), // textStyle: TextStyle( // color: resolveColorFromString(data?[':disabled']?['color']), // ), @@ -1147,7 +1157,7 @@ class GSConfigStyle extends BaseStyle { ), ), onDisabled: GSConfigStyle( - bg : resolveColorTokenFromString(data?[':disabled']?['bg']), + bg: resolveColorTokenFromString(data?[':disabled']?['bg']), borderColor: resolveColorTokenFromString( data?['_dark']?[':disabled']?['borderColor']), trackColorTrue: resolveColorTokenFromString( @@ -1260,7 +1270,7 @@ class GSConfigStyle extends BaseStyle { ? (data?['transform'].first as Map)['scale'] : null : null, - trackHeight: data?['_track'] != null + trackHeight: data?['_track'] != null ? (data?['_track']['height'] is int ? double.parse('${data?['_track']['height']}.0') : resolveSpaceFromString( @@ -1268,7 +1278,7 @@ class GSConfigStyle extends BaseStyle { data?['_track']['height'].toString(), )) : null, - trackWidth: data?['_track'] != null + trackWidth: data?['_track'] != null ? (data?['_track']['width'] is int ? double.parse('${data?['_track']['width']}.0') : resolveSpaceFromString( @@ -1292,6 +1302,13 @@ class GSConfigStyle extends BaseStyle { data?['_thumb']['w'].toString(), )) : null, + borderTopLeftRadius: data?['borderTopLeftRadius'] != null ? double.tryParse(data!['borderTopLeftRadius'].toString()) ?? + resolveRadiusFromString(data['borderTopLeftRadius'].toString()) : null, + borderTopRightRadius: data?['borderTopLeftRadius'] != null ? double.tryParse(data!['borderTopLeftRadius'].toString()) ?? + resolveRadiusFromString(data['borderTopLeftRadius'].toString()) : null, + + + iconColor: resolveColorTokenFromString( data?['_icon']?['color'] ?? data?['_selectedIcon']?['color'], ), diff --git a/lib/src/style/gs_style_external_inline.dart b/lib/src/style/gs_style_external_inline.dart index 9e398baa..08369387 100644 --- a/lib/src/style/gs_style_external_inline.dart +++ b/lib/src/style/gs_style_external_inline.dart @@ -46,6 +46,8 @@ class GSStyle extends BaseStyle { double? trackHeight; double? trackWidth; double? thumbHeight; + double? borderTopLeftRadius; + double? borderTopRightRadius; double? thumbWidth; GSCursors? cursors; GSPlacement? placement; @@ -123,6 +125,8 @@ class GSStyle extends BaseStyle { this.trackWidth, this.thumbHeight, this.thumbWidth, + this.borderTopLeftRadius, + this.borderTopRightRadius, this.outlineColor, this.cursors, this.iconSize, @@ -241,6 +245,10 @@ class GSStyle extends BaseStyle { trackWidth: overrideStyle?.trackWidth ?? trackWidth, thumbHeight: overrideStyle?.thumbHeight ?? thumbHeight, thumbWidth: overrideStyle?.thumbWidth ?? thumbWidth, + borderTopLeftRadius: + overrideStyle?.borderTopLeftRadius ?? borderTopLeftRadius, + borderTopRightRadius: + overrideStyle?.borderTopRightRadius ?? borderTopRightRadius, cursors: overrideStyle?.cursors ?? cursors, isVisible: overrideStyle?.isVisible ?? isVisible, direction: overrideStyle?.direction ?? direction, @@ -301,6 +309,8 @@ class GSStyle extends BaseStyle { trackWidth: styler.trackWidth, thumbHeight: styler.thumbHeight, thumbWidth: styler.thumbWidth, + borderTopLeftRadius: styler.borderTopLeftRadius, + borderTopRightRadius: styler.borderTopRightRadius, cursors: styler.cursors, placement: styler.placement, isVisible: styler.isVisible, diff --git a/lib/src/theme/config/enums.dart b/lib/src/theme/config/enums.dart index 7a7cdb7d..36fe96a2 100644 --- a/lib/src/theme/config/enums.dart +++ b/lib/src/theme/config/enums.dart @@ -48,6 +48,19 @@ enum GSInputSizes { $sm, } +enum GSSelectVariants { + underlined, + outline, + rounded, +} + +enum GSSelectSizes { + $sm, + $md, + $lg, + $xl, +} + enum GSAlertDialogSizes { $xs, $sm, @@ -299,4 +312,4 @@ enum GSSliderSizes { $sm, $md, $lg, -} \ No newline at end of file +} diff --git a/lib/src/theme/config/select/select.dart b/lib/src/theme/config/select/select.dart new file mode 100644 index 00000000..739e402c --- /dev/null +++ b/lib/src/theme/config/select/select.dart @@ -0,0 +1,7 @@ +const Map selectData = { + "width": '\$full', + "height": '\$full', + "_web": { + "pointerEvents": 'none', + }, +}; diff --git a/lib/src/theme/config/select/select_content.dart b/lib/src/theme/config/select/select_content.dart new file mode 100644 index 00000000..7ff8eda5 --- /dev/null +++ b/lib/src/theme/config/select/select_content.dart @@ -0,0 +1,14 @@ +const Map selectContentData = { + "alignItems": "center", + "borderTopLeftRadius": "\$3xl", + "borderTopRightRadius": "\$3xl", + "h": "\$full", + "p": "\$2", + "bg": "\$background0", + "_sectionHeaderBackground": {"bg": "\$background0"}, + "defaultProps": {"hardShadow": "5"}, + "_web": {"userSelect": "none", "pointerEvents": "auto"} +}; + // { + // descendantStyle: ['_sectionHeaderBackground'], + // } diff --git a/lib/src/theme/config/select/select_icon.dart b/lib/src/theme/config/select/select_icon.dart new file mode 100644 index 00000000..87dce66f --- /dev/null +++ b/lib/src/theme/config/select/select_icon.dart @@ -0,0 +1,60 @@ +const Map selectIconData = { + "variants": { + "size": { + "2xs": { + "h": "\$3", + "w": "\$3", + "props": { + // @ts-ignore + "size": 12 + } + }, + "xs": { + "h": "\$3.5", + "w": "\$3.5", + "props": { + // @ts-ignore + "size": 14 + } + }, + "sm": { + "h": "\$4", + "w": "\$4", + "props": { + // @ts-ignore + "size": 16 + } + }, + "md": { + "h": "\$4.5", + "w": "\$4.5", + "props": { + // @ts-ignore + "size": 18 + } + }, + "lg": { + "h": "\$5", + "w": "\$5", + "props": { + // @ts-ignore + "size": 20 + } + }, + "xl": { + "h": "\$6", + "w": "\$6", + "props": { + // @ts-ignore + "size": 24 + } + } + } + }, + "props": { + "size": "sm", + // @ts-ignore + "fill": "none" + }, + "color": "\$background500" +}; diff --git a/lib/src/theme/config/select/select_item.dart b/lib/src/theme/config/select/select_item.dart new file mode 100644 index 00000000..99ce90e6 --- /dev/null +++ b/lib/src/theme/config/select/select_item.dart @@ -0,0 +1,24 @@ +const Map selectItemData = { + "p": "\$3", + "flexDirection": "row", + "alignItems": "center", + "rounded": "\$sm", + "w": "\$full", + ":disabled": { + "opacity": 0.4, + "_web": { + // @ts-ignore + "pointerEvents": "all !important", + "cursor": "not-allowed" + } + }, + ":hover": {"bg": "\$background50"}, + ":active": {"bg": "\$background100"}, + ":focus": {"bg": "\$background100"}, + "_web": { + ":focusVisible": {"bg": "\$background100"} + } +}; + // { + // descendantStyle: ['_text', '_icon'], + // } diff --git a/lib/src/theme/config/select/select_select.dart b/lib/src/theme/config/select/select_select.dart new file mode 100644 index 00000000..d5a21774 --- /dev/null +++ b/lib/src/theme/config/select/select_select.dart @@ -0,0 +1 @@ +const Map selectSelectData = {}; diff --git a/lib/src/theme/config/select/select_selected_input.dart b/lib/src/theme/config/select/select_selected_input.dart new file mode 100644 index 00000000..4d912f57 --- /dev/null +++ b/lib/src/theme/config/select/select_selected_input.dart @@ -0,0 +1,19 @@ +const Map selectSelectedInputData = { + "_web": { + "w": '\$full', + }, + "pointerEvents": 'none', + "flex": 1, + "h": '\$full', + "color": '\$text900', + "props": { + "placeholderTextColor": '\$text500', + }, +}; + + // { + // ancestorStyle: ['_input'], + // resolveProps: ['placeholderTextColor'], + // } + + diff --git a/lib/src/theme/config/select/select_selection_header_text.dart b/lib/src/theme/config/select/select_selection_header_text.dart new file mode 100644 index 00000000..ee6cb01f --- /dev/null +++ b/lib/src/theme/config/select/select_selection_header_text.dart @@ -0,0 +1,89 @@ +const Map selectSelectionHeaderTextData = { + "letterSpacing": "\$sm", + "fontWeight": "\$bold", + "fontFamily": "\$heading", + + // Overrides expo-html default styling + "marginVertical": "0", + + "variants": { + "isTruncated": { + "true": { + "props": { + // @ts-ignore + "numberOfLines": "1", + "ellipsizeMode": "tail" + } + } + }, + "bold": { + "true": {"fontWeight": "\$bold"} + }, + "underline": { + "true": {"textDecorationLine": "underline"} + }, + "strikeThrough": { + "true": {"textDecorationLine": "line-through"} + }, + "size": { + "5xl": { + // @ts-ignore + "props": {"as": "H1"}, + "fontSize": "\$6xl" + }, + "4xl": { + // @ts-ignore + "props": {"as": "H1"}, + "fontSize": "\$5xl" + }, + "3xl": { + // @ts-ignore + "props": {"as": "H1"}, + "fontSize": "\$4xl" + }, + "2xl": { + // @ts-ignore + "props": {"as": "H2"}, + "fontSize": "\$3xl" + }, + "xl": { + // @ts-ignore + "props": {"as": "H3"}, + "fontSize": "\$2xl" + }, + "lg": { + // @ts-ignore + "props": {"as": "H4"}, + "fontSize": "\$xl" + }, + "md": { + // @ts-ignore + "props": {"as": "H5"}, + "fontSize": "\$lg" + }, + "sm": { + // @ts-ignore + "props": {"as": "H6"}, + "fontSize": "\$md" + }, + "xs": { + // @ts-ignore + "props": {"as": "H6"}, + "fontSize": "\$sm" + } + }, + "sub": { + "true": {"fontSize": "\$xs"} + }, + "italic": { + "true": {"fontStyle": "italic"} + }, + "highlight": { + "true": {"bg": "\$yellow500"} + } + }, + "color": "\$text500", + "props": {"size": "xs"}, + "textTransform": "uppercase", + "p": "\$3" +}; diff --git a/lib/src/theme/config/select/select_text.dart b/lib/src/theme/config/select/select_text.dart new file mode 100644 index 00000000..6bf517d4 --- /dev/null +++ b/lib/src/theme/config/select/select_text.dart @@ -0,0 +1,51 @@ +const Map selectTextData = { + "color": "\$text700", + "flex": "1", + "fontWeight": "\$normal", + "fontFamily": "\$body", + "fontStyle": "normal", + "letterSpacing": "\$md", + "variants": { + "isTruncated": { + "true": { + "props": { + // @ts-ignore + "numberOfLines": "1", + "ellipsizeMode": "tail" + } + } + }, + "bold": { + "true": {"fontWeight": "\$bold"} + }, + "underline": { + "true": {"textDecorationLine": "underline"} + }, + "strikeThrough": { + "true": {"textDecorationLine": "line-through"} + }, + "size": { + "2xs": {"fontSize": "\$2xs"}, + "xs": {"fontSize": "\$xs"}, + "sm": {"fontSize": "\$sm"}, + "md": {"fontSize": "\$md"}, + "lg": {"fontSize": "\$lg"}, + "xl": {"fontSize": "\$xl"}, + "2xl": {"fontSize": "\$2xl"}, + "3xl": {"fontSize": "\$3xl"}, + "4xl": {"fontSize": "\$4xl"}, + "5xl": {"fontSize": "\$5xl"}, + "6xl": {"fontSize": "\$6xl"} + }, + "sub": { + "true": {"fontSize": "\$xs"} + }, + "italic": { + "true": {"fontStyle": "italic"} + }, + "highlight": { + "true": {"bg": "\$yellow500"} + } + }, + "defaultProps": {"size": "md"} +}; \ No newline at end of file diff --git a/lib/src/theme/config/select/select_trigger.dart b/lib/src/theme/config/select/select_trigger.dart new file mode 100644 index 00000000..e805916d --- /dev/null +++ b/lib/src/theme/config/select/select_trigger.dart @@ -0,0 +1,143 @@ +const Map selectTriggerData = { + "borderWidth": 1, + "borderColor": "\$background300", + "borderRadius": "\$sm", + "flexDirection": "row", + "overflow": "hidden", + "alignItems": "center", + ":hover": {"borderColor": "\$border400"}, + ":focus": {"borderColor": "\$primary700"}, + ":disabled": { + "opacity": 0.4, + ":hover": {"borderColor": "\$background300"} + }, + "_input": {"py": "auto", "px": "\$3"}, + "_icon": {"color": "\$background500"}, + "variants": { + "size": { + "xl": { + "h": "\$12", + "_input": {"fontSize": "\$xl"}, + "_icon": {"h": "\$6", "w": "\$6"} + }, + "lg": { + "h": "\$11", + "_input": {"fontSize": "\$lg"}, + "_icon": {"h": "\$5", "w": "\$5"} + }, + "md": { + "h": "\$10", + "_input": {"fontSize": "\$md"}, + "_icon": {"h": "\$4", "w": "\$4"} + }, + "sm": { + "h": "\$9", + "_input": {"fontSize": "\$sm"}, + "_icon": {"h": "\$3.5", "w": "\$3.5"} + } + }, + "variant": { + "underlined": { + "_input": { + "_web": {"outlineWidth": "0", "outline": "none"}, + "px": "\$0" + }, + "borderWidth": "0", + "borderRadius": "0", + "borderBottomWidth": "\$1", + ":focus": { + "borderColor": "\$primary700", + "_web": {"boxShadow": "inset 0 -1px 0 0 \$primary700"}, + ":hover": { + "borderColor": "\$primary700", + "_web": {"boxShadow": "inset 0 -1px 0 0 \$primary600"} + } + }, + ":invalid": { + "borderBottomWidth": "2", + "borderBottomColor": "\$error700", + "_web": {"boxShadow": "inset 0 -1px 0 0 \$error700"}, + ":hover": {"borderBottomColor": "\$error700"}, + ":focus": { + "borderBottomColor": "\$error700", + ":hover": { + "borderBottomColor": "\$error700", + "_web": {"boxShadow": "inset 0 -1px 0 0 \$error700"} + } + }, + ":disabled": { + ":hover": { + "borderBottomColor": "\$error700", + "_web": {"boxShadow": "inset 0 -1px 0 0 \$error700"} + } + } + } + }, + "outline": { + "_input": { + "_web": {"outlineWidth": "0", "outline": "none"} + }, + ":focus": { + "borderColor": "\$primary700", + "_web": {"boxShadow": "inset 0 0 0 1px \$primary700"}, + ":hover": { + "borderColor": "\$primary700", + "_web": {"boxShadow": "inset 0 0 0 1px \$primary600"} + } + }, + ":invalid": { + "borderColor": "\$error700", + "_web": {"boxShadow": "inset 0 0 0 1px \$error700"}, + ":hover": {"borderColor": "\$error700"}, + ":focus": { + "borderColor": "\$error700", + ":hover": { + "borderColor": "\$error700", + "_web": {"boxShadow": "inset 0 0 0 1px \$error700"} + } + }, + ":disabled": { + ":hover": { + "borderColor": "\$error700", + "_web": {"boxShadow": "inset 0 0 0 1px \$error700"} + } + } + } + }, + "rounded": { + "borderRadius": "999", + "_input": { + "px": "\$4", + "_web": {"outlineWidth": "0", "outline": "none"} + }, + ":focus": { + "borderColor": "\$primary700", + "_web": {"boxShadow": "inset 0 0 0 1px \$primary700"}, + ":hover": { + "borderColor": "\$primary700", + "_web": {"boxShadow": "inset 0 0 0 1px \$primary600"} + } + }, + ":invalid": { + "borderColor": "\$error700", + "_web": {"boxShadow": "inset 0 0 0 1px \$error700"}, + ":hover": {"borderColor": "\$error700"}, + ":focus": { + "borderColor": "\$error700", + ":hover": { + "borderColor": "\$error700", + "_web": {"boxShadow": "inset 0 0 0 1px \$error700"} + } + }, + ":disabled": { + ":hover": { + "borderColor": "\$error700", + "_web": {"boxShadow": "inset 0 0 0 1px \$error700"} + } + } + } + } + } + }, + "defaultProps": {"size": "md", "variant": "outline"} +}; diff --git a/lib/src/utils/resolver.dart b/lib/src/utils/resolver.dart index 0f2e7efe..02b8cfc7 100644 --- a/lib/src/utils/resolver.dart +++ b/lib/src/utils/resolver.dart @@ -148,7 +148,7 @@ EdgeInsetsGeometry? resolvePaddingFromString(String? padding, String type, if (type == 'symmetric') { return EdgeInsets.symmetric( horizontal: resolveSpaceFromString(padding)!, - vertical: resolveSpaceFromString(paddingy)!); + vertical: resolveSpaceFromString(paddingy) ?? 0.0); } if (type == 'horizontal') { return EdgeInsets.symmetric(horizontal: resolveSpaceFromString(padding)!); diff --git a/lib/src/widgets/gs_app/gs_app.dart b/lib/src/widgets/gs_app/gs_app.dart index fab61f54..7c047e05 100644 --- a/lib/src/widgets/gs_app/gs_app.dart +++ b/lib/src/widgets/gs_app/gs_app.dart @@ -284,8 +284,8 @@ class _GSAppState extends State { localeListResolutionCallback: widget.localeListResolutionCallback, supportedLocales: widget.supportedLocales, showPerformanceOverlay: widget.showPerformanceOverlay, - checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, - checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, + // checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, + // checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, showSemanticsDebugger: widget.showSemanticsDebugger, debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, shortcuts: widget.shortcuts, @@ -310,8 +310,8 @@ class _GSAppState extends State { localeResolutionCallback: widget.localeResolutionCallback, supportedLocales: widget.supportedLocales, showPerformanceOverlay: widget.showPerformanceOverlay, - checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, - checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, + // checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, + // checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, showSemanticsDebugger: widget.showSemanticsDebugger, debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, shortcuts: widget.shortcuts, diff --git a/lib/src/widgets/gs_form_control/gs_form_control.dart b/lib/src/widgets/gs_form_control/gs_form_control.dart index bd4de956..fe5c19da 100644 --- a/lib/src/widgets/gs_form_control/gs_form_control.dart +++ b/lib/src/widgets/gs_form_control/gs_form_control.dart @@ -16,7 +16,7 @@ Form Control Compatible Components: class GSFormControl extends StatefulWidget { final GlobalKey formKey; final Widget child; - final PopInvokedCallback? onPopInvoked; + // final PopInvokedCallback? onPopInvoked; final VoidCallback? onChanged; final AutovalidateMode autovalidateMode; final bool? canPop; @@ -32,7 +32,7 @@ class GSFormControl extends StatefulWidget { const GSFormControl({ super.key, required this.child, - this.onPopInvoked, + // this.onPopInvoked, this.onChanged, this.autovalidateMode = AutovalidateMode.disabled, this.canPop, @@ -84,7 +84,7 @@ class _GSFormControlState extends State { key: widget.formKey, canPop: widget.canPop, onChanged: widget.onChanged, - onPopInvoked: widget.onPopInvoked, + // onPopInvoked: widget.onPopInvoked, autovalidateMode: widget.autovalidateMode, child: widget.child, ), diff --git a/lib/src/widgets/gs_radio/gs_radio_icon.dart b/lib/src/widgets/gs_radio/gs_radio_icon.dart index 9689cb76..6e8c1e58 100644 --- a/lib/src/widgets/gs_radio/gs_radio_icon.dart +++ b/lib/src/widgets/gs_radio/gs_radio_icon.dart @@ -2,8 +2,6 @@ import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; import 'package:gluestack_ui/src/style/style_resolver.dart'; import 'package:gluestack_ui/src/widgets/gs_radio/gs_radio_icon_style.dart'; -import 'package:gluestack_ui/src/widgets/gs_radio/gs_radio_raw.dart'; - class GSRadioIcon extends StatelessWidget { final Color? activeColor; final bool autofocus; diff --git a/lib/src/widgets/gs_radio/gs_radio_raw.dart b/lib/src/widgets/gs_radio/gs_radio_raw.dart index 4e29b440..00a078b5 100644 --- a/lib/src/widgets/gs_radio/gs_radio_raw.dart +++ b/lib/src/widgets/gs_radio/gs_radio_raw.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/cupertino.dart'; import 'package:gluestack_ui/gluestack_ui.dart'; -import 'gs_toggleable.dart'; const Size _size = Size(18.0, 18.0); const double _kOuterRadius = 8.0; diff --git a/lib/src/widgets/gs_radio/public.dart b/lib/src/widgets/gs_radio/public.dart index cec54ad8..35d80cfe 100644 --- a/lib/src/widgets/gs_radio/public.dart +++ b/lib/src/widgets/gs_radio/public.dart @@ -2,3 +2,4 @@ export 'gs_radio_icon.dart'; export 'gs_radio_provider.dart'; export 'gs_radio_text.dart'; export 'gs_radio.dart'; +export 'gs_radio_raw.dart'; diff --git a/lib/src/widgets/gs_select/gs_select.dart b/lib/src/widgets/gs_select/gs_select.dart new file mode 100644 index 00000000..6288dba4 --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select.dart @@ -0,0 +1,318 @@ +import 'package:gluestack_ui/gluestack_ui.dart'; +import 'package:gluestack_ui/src/style/style_resolver.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_icon_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_selection_header_text_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_text_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_trigger_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_style_builder/gs_style_builder.dart'; +import 'package:gluestack_ui/src/widgets/gs_text/gs_text_style.dart'; + +class GSSelect extends StatefulWidget { + final GSSelectSizes? size; + final GSSelectVariants? variant; + final GSStyle? style; + final GSSelectHeaderText label; + final GSSelectIcon icon; + final GSSelectContent content; + final ValueChanged? onValueChange; + final bool isDisabled; + final bool closeOnOverlayClick; + final Function()? onClose; + final Function()? onOpen; + final String? initialLabel; + + const GSSelect({ + super.key, + this.size, + this.variant, + required this.icon, + required this.label, + this.style, + this.onValueChange, + this.isDisabled = false, + this.closeOnOverlayClick = true, + required this.content, + this.onClose, + this.onOpen, + this.initialLabel, + }); + + @override + State createState() => _GSSelectState(); +} + +class _GSSelectState extends State { + String? selectedOption; + bool _isHovered = false; + int? hoveredIndex; + OverlayEntry? overlayEntry; + final LayerLink _layerLink = LayerLink(); + final GlobalKey _key = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + final Map _itemKeys = {}; + + @override + void initState() { + super.initState(); + if (widget.initialLabel != null && + widget.content.options.contains(widget.initialLabel)) { + selectedOption = widget.initialLabel; + } + } + + void removeOverlay() { + overlayEntry?.remove(); + overlayEntry = null; + hoveredIndex = null; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.onClose != null) { + widget.onClose!(); + } + }); + } + + void _selectOption(String option) { + setState(() { + selectedOption = option; + removeOverlay(); + if (widget.onValueChange != null) { + widget.onValueChange!(option); + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + int? _getSelectedOptionIndex() { + if (selectedOption == null) return null; + return widget.content.options.indexOf(selectedOption!); + } + + @override + Widget build(BuildContext context) { + return GSStyleBuilder( + child: Builder(builder: (context) { + final selectVariant = + widget.variant?.toGSVariant ?? selectTriggerStyle.props?.variant; + final selectSize = + widget.size?.toGSSize ?? selectTriggerStyle.props?.size; + final textSize = widget.size?.toGSSize ?? selectTextStyle.props?.size; + final iconSize = widget.size?.toGSSize ?? selectIconStyle.props?.size; + final headerFontSize = + widget.size?.toGSSize ?? selectSelectionHeaderTextStyle.props?.size; + + GSConfigStyle triggerStyler = resolveStyles( + context: context, + styles: [ + selectTriggerStyle, + selectTriggerStyle.variantMap(selectVariant), + selectTriggerStyle.sizeMap(selectSize), + ], + inlineStyle: widget.style, + ); + + GSConfigStyle textStyler = resolveStyles( + context: context, + styles: [ + gstextStyle, + selectTextStyle, + selectTextStyle.variantMap(selectVariant), + selectTextStyle.sizeMap(textSize), + ], + inlineStyle: widget.style, + ); + + Color? resolveBorderColor() { + if (_isHovered) { + return triggerStyler.onHover?.borderColor?.getColor(context) ?? + triggerStyler.borderColor?.getColor(context); + } else { + return triggerStyler.borderColor?.getColor(context); + } + } + + double? resolveBorderWidth() { + if (_isHovered) { + return triggerStyler.onHover?.borderWidth ?? + triggerStyler.borderWidth; + } else { + return triggerStyler.borderWidth; + } + } + + final borderColor = resolveBorderColor(); + final borderWidth = resolveBorderWidth(); + + final currentTextStyle = textStyler.textStyle?.copyWith( + decoration: textStyler.textStyle?.decoration ?? TextDecoration.none, + fontFamily: textStyler.textStyle?.fontFamily, + fontWeight: textStyler.textStyle?.fontWeight, + fontStyle: textStyler.textStyle?.fontStyle, + backgroundColor: textStyler.textStyle?.backgroundColor, + overflow: textStyler.textStyle?.overflow, + color: textStyler.color?.getColor(context) ?? + gstextStyle.color?.getColor(context), + ); + + void toggleDropdown() { + if (overlayEntry == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.onOpen != null) { + widget.onOpen!(); + } + }); + final renderBox = + _key.currentContext!.findRenderObject() as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + GestureDetector( + onTap: widget.closeOnOverlayClick ? removeOverlay : null, + behavior: HitTestBehavior.translucent, + child: Container( + color: const Color.fromARGB(0, 228, 9, 9), + ), + ), + Positioned( + left: offset.dx, + top: offset.dy + size.height, + width: size.width, + child: CompositedTransformFollower( + link: _layerLink, + showWhenUnlinked: false, + child: GSSelectProvider( + fontSize: triggerStyler.textStyle?.fontSize, + iconSize: iconSize, + scrollController: _scrollController, + selectVariant: selectVariant, + headerFontSize: headerFontSize, + textSize: textSize, + selectedOption: selectedOption, + removeOverlay: removeOverlay, + label: widget.label.text.toString(), + itemKeys: _itemKeys, + hoveredIndex: hoveredIndex, + selectOption: _selectOption, + currentTextStyle: currentTextStyle, + style: widget.style, + child: widget.content, + ), + ), + ), + ], + ), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + int? selectedIndex = _getSelectedOptionIndex(); + if (selectedIndex != null && selectedIndex > 0) { + final selectedKey = + _itemKeys[widget.content.options[selectedIndex]]; + if (selectedKey?.currentContext != null) { + Scrollable.ensureVisible(selectedKey!.currentContext!, + alignment: 0.5, + duration: const Duration(milliseconds: 300)); + } + } + } + }); + + Overlay.of(context).insert(overlayEntry!); + } else { + removeOverlay(); + } + } + + return GSAncestor( + decedentStyles: triggerStyler.descendantStyles, + child: Opacity( + opacity: widget.isDisabled == true ? 0.7 : 1, + child: IntrinsicWidth( + child: FocusableActionDetector( + onShowHoverHighlight: (value) { + widget.isDisabled == true + ? null + : setState(() => _isHovered = value); + }, + child: CompositedTransformTarget( + link: _layerLink, + child: GsGestureDetector( + key: _key, + onPressed: + widget.isDisabled == true ? null : toggleDropdown, + child: Container( + padding: widget.style?.padding ?? + triggerStyler.padding ?? + const EdgeInsets.symmetric(horizontal: 15), + height: triggerStyler.height, + width: triggerStyler.width, + decoration: BoxDecoration( + color: const Color.fromARGB(0, 0, 0, 0), + border: widget.variant == GSSelectVariants.underlined + ? Border( + bottom: BorderSide( + color: borderColor ?? + triggerStyler.borderColor! + .getColor(context), + width: borderWidth ?? + triggerStyler.borderBottomWidth ?? + 0, + ), + ) + : Border.all( + width: borderWidth ?? + triggerStyler.borderBottomWidth ?? + 0, + color: borderColor ?? + triggerStyler.borderColor! + .getColor(context), + ), + borderRadius: BorderRadius.circular( + triggerStyler.borderRadius ?? 0), + ), + child: GSSelectProvider( + fontSize: triggerStyler.textStyle?.fontSize, + iconSize: iconSize, + headerFontSize: headerFontSize, + selectOption: (String option) {}, + removeOverlay: removeOverlay, + scrollController: _scrollController, + itemKeys: _itemKeys, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + if (selectedOption != null) + Expanded( + child: Text( + selectedOption!, + overflow: TextOverflow.fade, + softWrap: false, + style: currentTextStyle, + ), + ) + else + widget.label, + widget.icon + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + }), + ); + } +} diff --git a/lib/src/widgets/gs_select/gs_select_content.dart b/lib/src/widgets/gs_select/gs_select_content.dart new file mode 100644 index 00000000..2df7603e --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_content.dart @@ -0,0 +1,249 @@ +import 'package:flutter/cupertino.dart'; +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/style_resolver.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_content_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_item_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_selected_input_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_text_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_text/gs_text_style.dart'; + +class GSSelectContent extends StatefulWidget { + final GSStyle? style; + final List options; + final List? disabledOptions; + final String? initialLabel; + + const GSSelectContent({ + super.key, + this.style, + required this.options, + this.disabledOptions, + this.initialLabel, + }); + + @override + State createState() => _GSSelectContentState(); +} + +class _GSSelectContentState extends State { + @override + Widget build(BuildContext context) { + final provider = GSSelectProvider.of(context); + final contentStyler = resolveStyles( + context: context, + styles: [selectContentStyle], + inlineStyle: widget.style, + ); + + GSConfigStyle styler = resolveStyles( + context: context, + styles: [selectStyle], + inlineStyle: widget.style, + ); + + GSConfigStyle textStyler = resolveStyles( + context: context, + styles: [ + gstextStyle, + selectTextStyle, + selectTextStyle.variantMap(provider?.selectVariant), + selectTextStyle.sizeMap(GSSizes.$md), + ], + inlineStyle: provider?.style, + ); + + final itemStyler = resolveStyles( + context: context, + styles: [ + selectItemStyle, + ], + inlineStyle: provider?.style, + ); + + final selectedInputStyler = resolveStyles( + context: context, + styles: [ + selectSelectedInputStyle, + ], + inlineStyle: provider?.style, + ); + + final inputTextStyle = textStyler.textStyle?.copyWith( + decoration: textStyler.textStyle?.decoration ?? TextDecoration.none, + fontFamily: widget.style?.textStyle?.fontFamily ?? + textStyler.textStyle?.fontFamily, + fontWeight: widget.style?.textStyle?.fontWeight ?? + textStyler.textStyle?.fontWeight, + fontStyle: widget.style?.textStyle?.fontStyle ?? + textStyler.textStyle?.fontStyle, + backgroundColor: widget.style?.textStyle?.backgroundColor ?? + textStyler.textStyle?.backgroundColor, + overflow: + widget.style?.textStyle?.overflow ?? textStyler.textStyle?.overflow, + color: widget.style?.textStyle?.color ?? + textStyler.color?.getColor(context) ?? + gstextStyle.color?.getColor(context), + fontSize: widget.style?.textStyle?.fontSize ?? + textStyler.textStyle?.fontSize); + + return Container( + height: widget.style?.height ?? styler.height ?? 200, + width: widget.style?.width ?? styler.width ?? 200, + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(widget.style?.borderRadius ?? 6)), + color: widget.style?.color ?? contentStyler.bg?.getColor(context), + ), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: provider?.scrollController, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: provider?.removeOverlay, + child: Container( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 5), + decoration: BoxDecoration( + color: const Color.fromARGB(0, 178, 20, 20), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(widget.style?.borderRadius ?? 6), + topRight: + Radius.circular(widget.style?.borderRadius ?? 6), + ), + ), + child: Row( + children: [ + if (provider?.selectedOption == null) + Icon( + CupertinoIcons.check_mark, + size: inputTextStyle?.fontSize ?? 16, + color: widget.style?.iconColor ?? + inputTextStyle?.color ?? + textStyler.color?.getColor(context), + ), + if (provider?.selectedOption != null) + Icon( + CupertinoIcons.check_mark, + size: inputTextStyle?.fontSize ?? 16, + color: const Color.fromARGB(0, 0, 0, 0), + ), + const SizedBox(width: 6.0), + Text( + provider?.label ?? "Select Option", + style: inputTextStyle?.copyWith( + color: inputTextStyle.color?.withOpacity( + selectedInputStyler.onDisabled?.opacity ?? 0.4, + ) ?? + textStyler.color?.getColor(context).withOpacity( + selectedInputStyler.onDisabled?.opacity ?? + 0.4, + ), + ), + ), + ], + ), + ), + ), + ...widget.options.asMap().entries.map((entry) { + int index = entry.key; + String option = entry.value; + final isDisabled = + widget.disabledOptions?.contains(option) ?? false; + provider?.itemKeys[option] = GlobalKey(); + + return StatefulBuilder( + key: provider?.itemKeys[option], + builder: (context, setState) { + return FocusableActionDetector( + onShowHoverHighlight: (value) { + if (!isDisabled) { + setState(() { + provider?.hoveredIndex = value ? index : null; + }); + } + }, + child: GsGestureDetector( + onPressed: () { + if (!isDisabled) { + provider?.selectOption(option); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Container( + decoration: BoxDecoration( + color: provider?.selectedOption == option + ? itemStyler.onActive?.bg?.getColor(context) + : (provider?.hoveredIndex == index + ? itemStyler.onHover?.bg + ?.getColor(context) + : const Color.fromARGB(0, 0, 0, 0)), + borderRadius: BorderRadius.all(Radius.circular( + widget.style?.borderRadius ?? 5)), + ), + padding: const EdgeInsets.symmetric( + vertical: 5, horizontal: 5), + child: Row( + children: [ + if (provider?.selectedOption == option) + Icon( + CupertinoIcons.check_mark, + size: inputTextStyle?.fontSize ?? 16, + color: widget.style?.iconColor ?? + inputTextStyle?.color ?? + textStyler.color?.getColor(context), + ), + if (provider?.selectedOption != option) + Icon( + CupertinoIcons.check_mark, + size: inputTextStyle?.fontSize ?? 16, + color: const Color.fromARGB(0, 0, 0, 0), + ), + const SizedBox(width: 6.0), + Expanded( + child: Text( + option, + overflow: TextOverflow.ellipsis, + style: inputTextStyle?.copyWith( + color: isDisabled + ? inputTextStyle.color?.withOpacity( + selectedInputStyler + .onDisabled?.opacity ?? + 0.4, + ) ?? + textStyler.color + ?.getColor(context) + .withOpacity( + selectedInputStyler + .onDisabled + ?.opacity ?? + 0.4, + ) + : inputTextStyle.color ?? + textStyler.color + ?.getColor(context) ?? + gstextStyle.color + ?.getColor(context), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/gs_select/gs_select_content_style.dart b/lib/src/widgets/gs_select/gs_select_content_style.dart new file mode 100644 index 00000000..0998c56c --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_content_style.dart @@ -0,0 +1,9 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectContentConfig = GSStyleConfig( + componentName: 'SelectContent', + descendantStyle: ['_sectionHeaderBackground']); +final GSConfigStyle selectContentStyle = GSConfigStyle.fromMap( + data: getIt().selectContent, + descendantStyle: gsSelectContentConfig.descendantStyle); diff --git a/lib/src/widgets/gs_select/gs_select_icon.dart b/lib/src/widgets/gs_select/gs_select_icon.dart new file mode 100644 index 00000000..10f4531a --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_icon.dart @@ -0,0 +1,71 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/style_resolver.dart'; +import 'package:gluestack_ui/src/widgets/gs_button/gs_button_icon_style.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_icon_style.dart'; + +/// This widget represents an icon which is optional with GSBadge widget. It's used to display icons along with text in GSBadge widget. +class GSSelectIcon extends StatelessWidget { + // The icon data to display in the widget. + final IconData iconData; + + /// The size of the icon, accepts [GSSizes]. + final GSSizes? iconSize; + // Custom style for the icon. + final GSStyle? style; + // Fill color for the icon. + final double? fill; + // Grade of the icon. + final double? grade; + // Optical size of the icon. + final double? opticalSize; + // Weight of the icon. + final double? weight; + // Semantic label for accessibility. + final String? semanticLabel; + // List of shadows for the icon. + final List? shadows; + // Text direction for the icon. + final TextDirection? textDirection; + + /// This widget represents an icon which is optional with GSBadge widget. It's used to display icons along with text in GSBadge widget. Following is the constructor for the same: + const GSSelectIcon( + {super.key, + required this.iconData, + this.iconSize, + this.style, + this.fill, + this.grade, + this.opticalSize, + this.weight, + this.semanticLabel, + this.shadows, + this.textDirection}); + + @override + Widget build(BuildContext context) { + final value = GSSelectProvider.of(context); + final size = GSButtonIconStyle + .size[iconSize ?? GSBadgeProvider.of(context)?.iconSize]; + final iconStyler = resolveStyles( + context: context, + styles: [ + selectIconStyle, + selectIconStyle.sizeMap(value?.iconSize), + ], + inlineStyle: style, + ); + + return Icon( + iconData, + size: size, + color: iconStyler.color?.getColor(context), + fill: fill, + grade: grade, + opticalSize: opticalSize, + weight: weight, + semanticLabel: semanticLabel, + textDirection: textDirection, + shadows: shadows, + ); + } +} diff --git a/lib/src/widgets/gs_select/gs_select_icon_style.dart b/lib/src/widgets/gs_select/gs_select_icon_style.dart new file mode 100644 index 00000000..7861dd9c --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_icon_style.dart @@ -0,0 +1,7 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectIconConfig = + GSStyleConfig(componentName: 'SelectIcon'); +final GSConfigStyle selectIconStyle = + GSConfigStyle.fromMap(data: getIt().selectIcon); diff --git a/lib/src/widgets/gs_select/gs_select_item_style.dart b/lib/src/widgets/gs_select/gs_select_item_style.dart new file mode 100644 index 00000000..b49427b2 --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_item_style.dart @@ -0,0 +1,10 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectItemConfig = GSStyleConfig( + componentName: 'SelectItem', + descendantStyle: ['_text', '_icon'] +); +final GSConfigStyle selectItemStyle = + GSConfigStyle.fromMap(data: getIt().selectItem, + descendantStyle: gsSelectItemConfig.descendantStyle); \ No newline at end of file diff --git a/lib/src/widgets/gs_select/gs_select_provider.dart b/lib/src/widgets/gs_select/gs_select_provider.dart new file mode 100644 index 00000000..fa2a9d02 --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_provider.dart @@ -0,0 +1,65 @@ +// ignore_for_file: must_be_immutable + +import 'package:gluestack_ui/gluestack_ui.dart'; + +/// GSSelectProvider is an InheritedWidget used to provide badge-related information to its descendants. +class GSSelectProvider extends InheritedWidget { + // Font size for the badge text. + final double? fontSize; + // Icon size for the badge icon. + final GSSizes? iconSize; + final GSSizes? headerFontSize; + final GSVariants? selectVariant; + final GSSizes? textSize; + final String? selectedOption; + final Function() removeOverlay; + final ScrollController scrollController; + final String? label; + final Map>> itemKeys; + int? hoveredIndex; + final void Function(String option) selectOption; + final TextStyle? currentTextStyle; + final GSStyle? style; + + /// Constructor for GSSelectProvider: + GSSelectProvider({ + super.key, + required this.fontSize, + required this.iconSize, + required super.child, + required this.headerFontSize, + this.selectVariant, + this.textSize, + this.selectedOption, + required this.removeOverlay, + required this.scrollController, + this.label, + required this.itemKeys, + this.hoveredIndex, + required this.selectOption, + this.currentTextStyle, + this.style, + }); + + /// Overrides the method to determine whether an update notification is needed. + @override + bool updateShouldNotify(GSSelectProvider oldWidget) { + return fontSize != oldWidget.fontSize || + iconSize != oldWidget.iconSize || + headerFontSize != oldWidget.headerFontSize || + selectVariant != oldWidget.selectVariant || + textSize != oldWidget.textSize || + selectedOption != oldWidget.selectedOption || + removeOverlay != oldWidget.removeOverlay || + scrollController != oldWidget.scrollController || + label != oldWidget.label || + style != oldWidget.style || + hoveredIndex != oldWidget.hoveredIndex || + currentTextStyle != oldWidget.currentTextStyle; + } + + /// Static method to obtain the GSSelectProvider instance from the given context. + static GSSelectProvider? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } +} diff --git a/lib/src/widgets/gs_select/gs_select_selected_input_style.dart b/lib/src/widgets/gs_select/gs_select_selected_input_style.dart new file mode 100644 index 00000000..49de05ca --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_selected_input_style.dart @@ -0,0 +1,9 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectSelectedInputConfig = GSStyleConfig( + componentName: 'SelectSelectedInput', + ancestorStyle: ['_input'] +); +final GSConfigStyle selectSelectedInputStyle = + GSConfigStyle.fromMap(data: getIt().selectSelectedInput); \ No newline at end of file diff --git a/lib/src/widgets/gs_select/gs_select_selection_header_text.dart b/lib/src/widgets/gs_select/gs_select_selection_header_text.dart new file mode 100644 index 00000000..51747150 --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_selection_header_text.dart @@ -0,0 +1,40 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/style_resolver.dart'; +import 'package:gluestack_ui/src/widgets/gs_select/gs_select_selection_header_text_style.dart'; + +/// GSSelectHeaderText is a Flutter widget that displays a text within a GSBadge widget. +class GSSelectHeaderText extends StatelessWidget { + // The text to be displayed inside the badge. + final String text; + // Style for the badge text. Can be customized using GSStyle. + final GSStyle? style; + + /// Constructor for GSSelectHeaderText widget: + const GSSelectHeaderText( + //takes string as input just like inbuilt Text widget from flutter + this.text, { + super.key, + this.style, + }); + + @override + Widget build(BuildContext context) { + final value = GSSelectProvider.of(context); + + final styler = resolveStyles( + context: context, + styles: [ + selectSelectionHeaderTextStyle, + selectSelectionHeaderTextStyle.sizeMap(value?.headerFontSize), + // ancestorStyles, + ], + inlineStyle: style, + ); + + return Text( + text, + style: styler.textStyle + ?.merge(TextStyle(color: styler.color?.getColor(context))), + ); + } +} diff --git a/lib/src/widgets/gs_select/gs_select_selection_header_text_style.dart b/lib/src/widgets/gs_select/gs_select_selection_header_text_style.dart new file mode 100644 index 00000000..9dd7b4ce --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_selection_header_text_style.dart @@ -0,0 +1,8 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectSelectionHeaderTextConfig = GSStyleConfig( + componentName: 'SelectSelectionHeaderText', +); +final GSConfigStyle selectSelectionHeaderTextStyle = GSConfigStyle.fromMap( + data: getIt().selectSelectionHeaderText); diff --git a/lib/src/widgets/gs_select/gs_select_style.dart b/lib/src/widgets/gs_select/gs_select_style.dart new file mode 100644 index 00000000..951592d3 --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_style.dart @@ -0,0 +1,8 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectConfig = GSStyleConfig( + componentName: 'Select', +); +final GSConfigStyle selectStyle = + GSConfigStyle.fromMap(data: getIt().select); \ No newline at end of file diff --git a/lib/src/widgets/gs_select/gs_select_text_style.dart b/lib/src/widgets/gs_select/gs_select_text_style.dart new file mode 100644 index 00000000..a6ae4364 --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_text_style.dart @@ -0,0 +1,9 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectTextConfig = GSStyleConfig( + componentName: 'SelectText', + ancestorStyle: ['_text'], +); +final GSConfigStyle selectTextStyle = + GSConfigStyle.fromMap(data: getIt().selectText); diff --git a/lib/src/widgets/gs_select/gs_select_trigger_style.dart b/lib/src/widgets/gs_select/gs_select_trigger_style.dart new file mode 100644 index 00000000..cab601cb --- /dev/null +++ b/lib/src/widgets/gs_select/gs_select_trigger_style.dart @@ -0,0 +1,11 @@ +import 'package:gluestack_ui/src/style/gs_config_style_internal.dart'; +import 'package:gluestack_ui/src/style/gs_style_config.dart'; + +const GSStyleConfig gsSelectTriggerConfig = GSStyleConfig( + componentName: 'SelectTrigger', + descendantStyle: ['_input', '_icon'], +); +final GSConfigStyle selectTriggerStyle = + GSConfigStyle.fromMap(data: getIt().selectTrigger, + descendantStyle: gsSelectTriggerConfig.descendantStyle + ); diff --git a/lib/src/widgets/gs_select/public.dart b/lib/src/widgets/gs_select/public.dart new file mode 100644 index 00000000..1fae4dbb --- /dev/null +++ b/lib/src/widgets/gs_select/public.dart @@ -0,0 +1,5 @@ +export 'gs_select.dart'; +export 'gs_select_provider.dart'; +export 'gs_select_icon.dart'; +export 'gs_select_content.dart'; +export 'gs_select_selection_header_text.dart'; \ No newline at end of file