diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddd87d9..02302eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.6.0 + +- Add `ShadTabs` component +- Add `ShadColorScheme.fromName` to easily create a color scheme from a name (String) +- Add `package` to `ShadImage` (thanks to @farhanfadila1717) +- Fix `decoration` of form fields +- Fix selection controls of `ShadInput` + ## 0.5.7 - Renamed the breakpoints diff --git a/Makefile b/Makefile index b4172bbd..0e903adb 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ compile: - cd playground && flutter build web --web-renderer canvaskit + cd playground && flutter build web --web-renderer canvaskit --no-tree-shake-icons deploy: cd playground && firebase deploy diff --git a/README.md b/README.md index 3c1a8cbf..e74bc102 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ See the [documentation](https://mariuti.com/shadcn-ui/) to interact with the com - [ ] Sonner (?) - [x] [Switch](https://mariuti.com/shadcn-ui/components/switch/) - [x] [Table](https://mariuti.com/shadcn-ui/components/table/) -- [ ] Tabs +- [x] [Tabs](https://mariuti.com/shadcn-ui/components/tabs/) - [ ] TextArea - [x] [Toast](https://mariuti.com/shadcn-ui/components/toast/) - [ ] Toggle diff --git a/example/lib/common/base_scaffold.dart b/example/lib/common/base_scaffold.dart index 0d32497e..e1016b70 100644 --- a/example/lib/common/base_scaffold.dart +++ b/example/lib/common/base_scaffold.dart @@ -12,6 +12,7 @@ class BaseScaffold extends StatelessWidget { this.crossAxisAlignment = CrossAxisAlignment.center, this.wrapChildrenInScrollable = true, this.wrapSingleChildInColumn = true, + this.alignment, }); final List children; @@ -20,19 +21,22 @@ class BaseScaffold extends StatelessWidget { final CrossAxisAlignment crossAxisAlignment; final bool wrapChildrenInScrollable; final bool wrapSingleChildInColumn; + final Alignment? alignment; @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; final size = MediaQuery.sizeOf(context); - Widget left = Center( - child: children.length == 1 && !wrapSingleChildInColumn - ? children[0] - : Column( - crossAxisAlignment: crossAxisAlignment, - children: children.separatedBy(const SizedBox(height: 8)), - )); + Widget left = Align( + alignment: alignment ?? Alignment.center, + child: children.length == 1 && !wrapSingleChildInColumn + ? children[0] + : Column( + crossAxisAlignment: crossAxisAlignment, + children: children.separatedBy(const SizedBox(height: 8)), + ), + ); if (wrapChildrenInScrollable) { left = SingleChildScrollView( diff --git a/example/lib/main.dart b/example/lib/main.dart index 75623e4f..528a8193 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,6 +22,7 @@ import 'package:example/pages/sheet.dart'; import 'package:example/pages/slider.dart'; import 'package:example/pages/switch.dart'; import 'package:example/pages/switch_form_field.dart'; +import 'package:example/pages/tabs.dart'; import 'package:example/pages/table.dart'; import 'package:example/pages/toast.dart'; import 'package:example/pages/tooltip.dart'; @@ -53,17 +54,18 @@ final routes = { '/progress': (_) => const ProgressPage(), '/radio-group': (_) => const RadioPage(), '/radio-group-form-field': (_) => const RadioGroupFormFieldPage(), + '/resizable': (_) => const ResizablePage(), '/select': (_) => const SelectPage(), '/select-form-field': (_) => const SelectFormFieldPage(), '/sheet': (_) => const SheetPage(), '/slider': (_) => const SliderPage(), '/switch': (_) => const SwitchPage(), '/switch-form-field': (_) => const SwitchFormFieldPage(), + '/tabs': (_) => const TabsPage(), '/table': (_) => const TablePage(), '/toast': (_) => const ToastPage(), '/tooltip': (_) => const TooltipPage(), '/typography': (_) => const TypographyPage(), - '/resizable': (_) => const ResizablePage(), }; final routeToNameRegex = RegExp('(?:^/|-)([a-z])'); diff --git a/example/lib/pages/tabs.dart b/example/lib/pages/tabs.dart new file mode 100644 index 00000000..dcf65016 --- /dev/null +++ b/example/lib/pages/tabs.dart @@ -0,0 +1,77 @@ +import 'package:example/common/base_scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +class TabsPage extends StatelessWidget { + const TabsPage({super.key}); + + @override + Widget build(BuildContext context) { + return BaseScaffold( + appBarTitle: "Tabs", + wrapChildrenInScrollable: false, + wrapSingleChildInColumn: false, + alignment: Alignment.topCenter, + children: [ + ShadTabs( + defaultValue: 'account', + tabBarConstraints: const BoxConstraints(maxWidth: 400), + contentConstraints: const BoxConstraints(maxWidth: 400), + tabs: [ + ShadTab( + value: 'account', + text: const Text('Account'), + content: ShadCard( + title: const Text('Account'), + description: const Text( + "Make changes to your account here. Click save when you're done."), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Name'), + initialValue: 'Ale', + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('Username'), + initialValue: 'nank1ro', + ), + const SizedBox(height: 16), + ], + ), + footer: const ShadButton(text: Text('Save changes')), + ), + ), + ShadTab( + value: 'password', + text: const Text('Password'), + content: ShadCard( + title: const Text('Password'), + description: const Text( + "Change your password here. After saving, you'll be logged out."), + content: Column( + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Current password'), + obscureText: true, + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('New password'), + obscureText: true, + ), + const SizedBox(height: 16), + ], + ), + footer: const ShadButton(text: Text('Save password')), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/shadcn_ui.dart b/lib/shadcn_ui.dart index 52beb00e..3f1e5a77 100644 --- a/lib/shadcn_ui.dart +++ b/lib/shadcn_ui.dart @@ -24,14 +24,15 @@ export 'src/components/input.dart'; export 'src/components/popover.dart'; export 'src/components/progress.dart'; export 'src/components/radio.dart'; +export 'src/components/resizable.dart'; export 'src/components/select.dart'; export 'src/components/sheet.dart'; export 'src/components/slider.dart'; export 'src/components/switch.dart'; +export 'src/components/tabs.dart'; export 'src/components/table.dart'; export 'src/components/toast.dart'; export 'src/components/tooltip.dart'; -export 'src/components/resizable.dart'; // Raw Components export 'src/raw_components/portal.dart'; @@ -81,6 +82,7 @@ export 'src/theme/components/tooltip.dart'; export 'src/theme/text_theme/text_styles_default.dart'; export 'src/theme/text_theme/theme.dart'; export 'src/theme/components/resizable.dart'; +export 'src/theme/components/tabs.dart'; // Utils export 'src/utils/position.dart'; diff --git a/lib/src/components/form/field.dart b/lib/src/components/form/field.dart index 9a7ea49b..0cbf1b23 100644 --- a/lib/src/components/form/field.dart +++ b/lib/src/components/form/field.dart @@ -31,9 +31,15 @@ class ShadFormBuilderField extends FormField { hasError ? error ?? Text(field.errorText!) : null; return ShadDecorator( - // The decoration is set to none because the component itself has - // its own decoration - decoration: ShadDecoration.none, + // borders are handled by the field itself + decoration: const ShadDecoration( + border: ShadBorder.none, + secondaryBorder: ShadBorder.none, + errorBorder: ShadBorder.none, + focusedBorder: ShadBorder.none, + secondaryErrorBorder: ShadBorder.none, + secondaryFocusedBorder: ShadBorder.none, + ), label: label, hasError: hasError, error: effectiveError, diff --git a/lib/src/components/image.dart b/lib/src/components/image.dart index c657e6f6..3b4c62ce 100644 --- a/lib/src/components/image.dart +++ b/lib/src/components/image.dart @@ -35,6 +35,7 @@ class ShadImage extends StatelessWidget { this.antialiasing = true, this.semanticLabel, this.svgTheme, + this.package, }) : assert( src is String || src is IconData, 'src must be a String or IconData', @@ -53,6 +54,7 @@ class ShadImage extends StatelessWidget { this.antialiasing = true, this.semanticLabel, this.svgTheme, + this.package, }) : width = size, height = size, assert( @@ -101,6 +103,9 @@ class ShadImage extends StatelessWidget { /// The theme of the svg final SvgTheme? svgTheme; + /// The package of the image, if any. + final String? package; + /// Returns `true` if the image is remote. bool get isRemote => Uri.tryParse(src as String)?.host.isNotEmpty ?? false; @@ -135,15 +140,15 @@ class ShadImage extends StatelessWidget { // // Finally, if there is a [gradient], apply a shader mask to the image. if (isRemote) { - if (isSvg) { - image = SvgPicture.network( - sourceString, + if (isSvgVector) { + image = SvgPicture( + NetworkBytesLoader(Uri.parse(sourceString)), width: width, height: height, fit: fit, + alignment: alignment, colorFilter: colorFilter, clipBehavior: Clip.antiAlias, - alignment: alignment, placeholderBuilder: placeholder != null ? (_) => placeholder! : null, semanticsLabel: semanticLabel, @@ -181,7 +186,7 @@ class ShadImage extends StatelessWidget { } } else if (isSvgVector) { image = SvgPicture( - AssetBytesLoader(sourceString), + AssetBytesLoader(sourceString, packageName: package), width: width, height: height, fit: fit, @@ -202,6 +207,7 @@ class ShadImage extends StatelessWidget { alignment: alignment, placeholderBuilder: placeholder != null ? (_) => placeholder! : null, semanticsLabel: semanticLabel, + package: package, ); } else { image = Image.asset( @@ -219,6 +225,7 @@ class ShadImage extends StatelessWidget { return child; }, semanticLabel: semanticLabel, + package: package, ); } } diff --git a/lib/src/components/input.dart b/lib/src/components/input.dart index ed2744ff..b72b5bc8 100644 --- a/lib/src/components/input.dart +++ b/lib/src/components/input.dart @@ -1,5 +1,6 @@ import 'dart:ui' as ui; +import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -299,6 +300,19 @@ class ShadInputState extends State final effectiveMouseCursor = widget.mouseCursor ?? WidgetStateMouseCursor.textable; + final defaultSelectionControls = switch (Theme.of(context).platform) { + TargetPlatform.iOS => cupertinoTextSelectionHandleControls, + TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls, + TargetPlatform.android || + TargetPlatform.fuchsia => + materialTextSelectionHandleControls, + TargetPlatform.linux || + TargetPlatform.windows => + desktopTextSelectionHandleControls, + }; + final effectiveSelectionControls = + widget.selectionControls ?? defaultSelectionControls; + return ShadDisabled( disabled: !widget.enabled, child: _selectionGestureDetectorBuilder.buildGestureDetector( @@ -408,7 +422,7 @@ class ShadInputState extends State contextMenuBuilder: widget.contextMenuBuilder, selectionControls: - widget.selectionControls, + effectiveSelectionControls, mouseCursor: effectiveMouseCursor, enableInteractiveSelection: widget.enableInteractiveSelection, @@ -417,6 +431,7 @@ class ShadInputState extends State widget.spellCheckConfiguration, textAlign: widget.textAlign, onTapOutside: widget.onTapOutside, + rendererIgnoresPointer: true, ), ), ), diff --git a/lib/src/components/tabs.dart b/lib/src/components/tabs.dart new file mode 100644 index 00000000..3854b9c8 --- /dev/null +++ b/lib/src/components/tabs.dart @@ -0,0 +1,765 @@ +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/src/components/button.dart'; +import 'package:shadcn_ui/src/theme/components/decorator.dart'; +import 'package:shadcn_ui/src/theme/theme.dart'; +import 'package:shadcn_ui/src/theme/themes/shadows.dart'; +import 'package:shadcn_ui/src/utils/gesture_detector.dart'; +import 'package:shadcn_ui/src/utils/states_controller.dart'; + +class ShadTabsInheritedWidget extends InheritedWidget { + const ShadTabsInheritedWidget({ + super.key, + required this.data, + required super.child, + }); + + final ShadTabsState data; + + static ShadTabsState of(BuildContext context) { + final provider = maybeOf(context); + if (provider == null) { + throw FlutterError('No ShadTabs widget found in context'); + } + return provider; + } + + static ShadTabsState? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType>() + ?.data; + } + + @override + bool updateShouldNotify(covariant ShadTabsInheritedWidget oldWidget) { + return true; + } +} + +class ShadTabsController extends ChangeNotifier { + ShadTabsController({required T defaultValue}) : selected = defaultValue; + + T selected; + + void select(T value) { + if (value == selected) return; + selected = value; + notifyListeners(); + } +} + +class RestorableShadTabsController + extends RestorableChangeNotifier> { + /// Creates a [RestorableShadTabsController]. + factory RestorableShadTabsController({required T defaultValue}) => + RestorableShadTabsController.fromValue(defaultValue); + + RestorableShadTabsController.fromValue(T value) : selected = value; + + T selected; + + @override + ShadTabsController createDefaultValue() { + return ShadTabsController(defaultValue: selected); + } + + @override + ShadTabsController fromPrimitives(Object? data) { + return ShadTabsController(defaultValue: data! as T); + } + + @override + Object? toPrimitives() { + return selected; + } +} + +class ShadTabs extends StatefulWidget implements PreferredSizeWidget { + const ShadTabs({ + super.key, + this.defaultValue, + required this.tabs, + this.controller, + this.gap, + this.scrollable, + this.dragStartBehavior, + this.physics, + this.padding, + this.decoration, + this.tabBarConstraints, + this.contentConstraints, + this.expandContent, + this.restorationId, + }) : assert( + (defaultValue != null) ^ (controller != null), + 'Either defaultValue or controller must be provided', + ); + + /// {@template ShadTabs.defaultValue} + /// The currently selected tab. + /// {@endtemplate} + final T? defaultValue; + + /// {@template ShadTabs.tabs} + /// The tabs to display. + /// {@endtemplate} + final List> tabs; + + /// {@template ShadTabs.controller} + /// The controller of the tabs. + /// {@endtemplate} + final ShadTabsController? controller; + + /// {@template ShadTabs.gap} + /// The gap between the tabBar and the content. + /// {@endtemplate} + final double? gap; + + /// {@template ShadTabs.scrollable} + /// Whether the tabs should be scrollable, defaults to false. + /// {@endtemplate} + final bool? scrollable; + + /// {@template ShadTabs.dragStartBehavior} + /// The drag start behavior of the tabs, defaults to [DragStartBehavior.start] + /// {@endtemplate} + final DragStartBehavior? dragStartBehavior; + + /// {@template ShadTabs.physics} + /// The physics of the tabs, defaults to null. + /// {@endtemplate} + final ScrollPhysics? physics; + + /// {@template ShadTabs.padding} + /// The padding of the tabs, defaults to `EdgeInsets.zero`. + /// {@endtemplate} + final EdgeInsets? padding; + + /// {@template ShadTabs.decoration} + /// The decoration of the tabs. + /// {@endtemplate} + final ShadDecoration? decoration; + + /// {@template ShadTabs.tabBarConstraints} + /// The constraints of the tab bar, defaults to `null`. + /// {@endtemplate} + final BoxConstraints? tabBarConstraints; + + /// {@template ShadTabs.contentConstraints} + /// The constraints of the content, defaults to `null`. + /// {@endtemplate} + final BoxConstraints? contentConstraints; + + /// {@template ShadTabs.expandContent} + /// Whether the content should be expanded, defaults to `false`. + /// {@endtemplate} + final bool? expandContent; + + /// {@template ShadTabs.restorationId} + /// The restoration id, defaults to `null`. + /// {@endtemplate} + final String? restorationId; + + @override + State> createState() => ShadTabsState(); + + @override + Size get preferredSize { + var maxHeight = 0.0; + for (final tab in tabs) { + final itemHeight = tab.preferredSize.height; + maxHeight = math.max(itemHeight, maxHeight); + } + return Size.fromHeight(maxHeight); + } +} + +class ShadTabsState extends State> with RestorationMixin { + late List _tabKeys; + + late List orderedValues; + + RestorableShadTabsController? _controller; + + ShadTabsController get controller => + widget.controller ?? _controller!.value; + + late final scrollController = ScrollController(); + + bool get scrollable => widget.scrollable ?? false; + + @override + void initState() { + super.initState(); + _tabKeys = widget.tabs.map((_) => GlobalKey()).toList(); + if (widget.controller == null) { + _createLocalController(widget.defaultValue as T); + } + orderedValues = widget.tabs.map((e) => e.value).toList(); + } + + @override + void didUpdateWidget(covariant ShadTabs oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.controller == null && oldWidget.controller != null) { + _createLocalController(oldWidget.controller!.selected); + } else if (widget.controller != null && oldWidget.controller == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + } + + if (widget.tabs.length > _tabKeys.length) { + final delta = widget.tabs.length - _tabKeys.length; + _tabKeys.addAll(List.generate(delta, (int n) => GlobalKey())); + } else if (widget.tabs.length < _tabKeys.length) { + _tabKeys.removeRange(widget.tabs.length, _tabKeys.length); + } + if (!listEquals(widget.tabs, oldWidget.tabs)) { + orderedValues = widget.tabs.map((e) => e.value).toList(); + } + } + + @override + void dispose() { + scrollController.dispose(); + _controller?.dispose(); + super.dispose(); + } + + void _registerController() { + assert(_controller != null); + registerForRestoration(_controller!, 'controller'); + } + + void _createLocalController(T value) { + assert(_controller == null); + _controller = RestorableShadTabsController.fromValue(value); + if (!restorePending) { + _registerController(); + } + } + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (_controller != null) _registerController(); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final tabsTheme = theme.tabsTheme; + + final effectiveDragStartBehavior = widget.dragStartBehavior ?? + tabsTheme.dragStartBehavior ?? + DragStartBehavior.start; + + final effectivePadding = + widget.padding ?? tabsTheme.padding ?? EdgeInsets.zero; + + final effectiveDecoration = widget.decoration ?? + tabsTheme.decoration ?? + ShadDecoration( + merge: false, + color: theme.colorScheme.muted, + border: ShadBorder(radius: theme.radius), + ); + + final effectiveGap = widget.gap ?? tabsTheme.gap ?? 8; + + final effectiveTabBarConstraints = + widget.tabBarConstraints ?? tabsTheme.tabBarConstraints; + + final effectiveContentConstraints = + widget.contentConstraints ?? tabsTheme.contentConstraints; + + final effectiveExpandContent = + widget.expandContent ?? tabsTheme.expandContent ?? false; + + Widget tabBar = Row(children: widget.tabs); + + if (effectiveTabBarConstraints != null) { + tabBar = ConstrainedBox( + constraints: effectiveTabBarConstraints, + child: tabBar, + ); + } + + if (scrollable) { + tabBar = ScrollConfiguration( + // The scrolling tabs should not show an overscroll indicator. + behavior: ScrollConfiguration.of(context).copyWith(overscroll: false), + child: SingleChildScrollView( + dragStartBehavior: effectiveDragStartBehavior, + scrollDirection: Axis.horizontal, + controller: scrollController, + padding: effectivePadding, + physics: widget.physics, + child: tabBar, + ), + ); + } else { + tabBar = Padding(padding: effectivePadding, child: tabBar); + } + + return ShadTabsInheritedWidget( + data: this, + child: ListenableBuilder( + listenable: controller, + builder: (context, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ShadDecorator( + decoration: effectiveDecoration, + child: tabBar, + ), + SizedBox(height: effectiveGap), + ...List.generate(widget.tabs.length, (int index) { + final tab = widget.tabs[index]; + final selected = tab.value == controller.selected; + Widget content = Offstage( + offstage: !selected, + child: FocusTraversalGroup( + descendantsAreFocusable: selected, + policy: WidgetOrderTraversalPolicy(), + child: KeyedSubtree( + key: _tabKeys[index], + child: tab.content, + ), + ), + ); + + if (effectiveContentConstraints != null) { + content = ConstrainedBox( + constraints: effectiveContentConstraints, + child: content, + ); + } + + if (effectiveExpandContent && selected) { + content = Expanded(child: content); + } + + return content; + }), + ], + ); + }, + ), + ); + } +} + +class ShadTab extends StatefulWidget implements PreferredSizeWidget { + const ShadTab({ + super.key, + required this.value, + required this.text, + required this.content, + this.icon, + this.enabled = true, + this.flex = 1, + this.height, + this.width, + this.backgroundColor, + this.selectedBackgroundColor, + this.hoverBackgroundColor, + this.selectedHoverBackgroundColor, + this.padding, + this.decoration, + this.selectedDecoration, + this.foregroundColor, + this.selectedForegroundColor, + this.textStyle, + this.shadows, + this.selectedShadows, + this.focusNode, + this.onPressed, + this.onLongPress, + this.size, + this.applyIconColorFilter, + this.cursor, + this.hoverForegroundColor, + this.autofocus = false, + this.pressedBackgroundColor, + this.pressedForegroundColor, + this.gradient, + this.textDecoration, + this.hoverTextDecoration, + this.statesController, + this.mainAxisAlignment, + this.crossAxisAlignment, + this.hoverStrategies, + this.onHoverChange, + this.onTapDown, + this.onTapUp, + this.onTapCancel, + this.onLongPressStart, + this.onLongPressCancel, + this.onLongPressUp, + this.onLongPressDown, + this.onLongPressEnd, + this.onDoubleTap, + this.onDoubleTapDown, + this.onDoubleTapCancel, + this.longPressDuration, + }); + + /// The value of the tab. + final T value; + + /// The text of the tab. + final Widget text; + + /// The content of the tab. + final Widget content; + + /// The icon of the tab. + final Widget? icon; + + /// Whether the tab is enabled, defaults to true. + final bool enabled; + + /// The flex of the tab, defaults to 1. + /// + /// If the tab is scrollable, the flex is ignored. + final int flex; + + /// The height of the tab, defaults to 32. + final double? height; + + /// The width of the tab, defaults to null when [ShadTabs.scrollable] is true, + /// otherwise `double.infinity`. + final double? width; + + /// The background color of the unselected tab, defaults to + /// `Colors.transparent`. + final Color? backgroundColor; + + /// The background color of the selected tab, defaults to + /// [ShadThemeData.colorScheme.background]. + final Color? selectedBackgroundColor; + + /// The background color of the hovered tab, defaults to + /// [ShadTab.backgroundColor]. + final Color? hoverBackgroundColor; + + /// The background color of the selected tab, defaults to + /// [ShadTab.selectedBackgroundColor]. + final Color? selectedHoverBackgroundColor; + + /// The padding of the tab, defaults to + /// `EdgeInsets.symmetric(horizontal: 12, vertical: 6)`. + final EdgeInsets? padding; + + /// The decoration of the tab. + final ShadDecoration? decoration; + + /// The decoration of the selected tab, defaults to [ShadTab.decoration]. + final ShadDecoration? selectedDecoration; + + /// The foreground color of the unselected tab, defaults to + /// [ShadThemeData.colorScheme.foreground]. + final Color? foregroundColor; + + /// The foreground color of the selected tab, defaults to + /// [ShadTab.foregroundColor]. + final Color? selectedForegroundColor; + + /// The text style of the tab, defaults to [ShadThemeData.textTheme.small]. + final TextStyle? textStyle; + + /// The shadows of the unselected tab, defaults to [ShadShadows.sm]. + final List? shadows; + + /// The shadows of the selected tab, defaults to `null`. + final List? selectedShadows; + + /// The focus node of the tab. + final FocusNode? focusNode; + + /// The callback that is called when the button is tapped. + final VoidCallback? onPressed; + + /// The callback that is called when the button is long-pressed. + final VoidCallback? onLongPress; + + /// The size of the button. + final ShadButtonSize? size; + + /// Whether to apply the icon color filter to the button. + final bool? applyIconColorFilter; + + /// The cursor for the button. + final MouseCursor? cursor; + + /// The foreground color of the button when the mouse is hovering over it. + final Color? hoverForegroundColor; + + /// Whether the button should automatically focus itself. + final bool autofocus; + + /// The background color of the button when it is pressed. + final Color? pressedBackgroundColor; + + /// The foreground color of the button when it is pressed. + final Color? pressedForegroundColor; + + /// The gradient to use for the button's background. + final Gradient? gradient; + + /// The text decoration to use for the button's text. + final TextDecoration? textDecoration; + + /// The text decoration to use for the button's text when the mouse is + /// hovering over it. + final TextDecoration? hoverTextDecoration; + + /// The states controller of the button. + final ShadStatesController? statesController; + + /// {@template ShadButton.mainAxisAlignment} + /// The main axis alignment of the button. + /// + /// Defaults to [MainAxisAlignment.center] + /// {@endtemplate} + final MainAxisAlignment? mainAxisAlignment; + + /// {@template ShadButton.crossAxisAlignment} + /// The cross axis alignment of the button. + /// + /// Defaults to [CrossAxisAlignment.center] + /// {@endtemplate} + final CrossAxisAlignment? crossAxisAlignment; + + final ShadHoverStrategies? hoverStrategies; + final ValueChanged? onHoverChange; + final ValueChanged? onTapDown; + final ValueChanged? onTapUp; + final VoidCallback? onTapCancel; + final ValueChanged? onLongPressStart; + final VoidCallback? onLongPressCancel; + final VoidCallback? onLongPressUp; + final ValueChanged? onLongPressDown; + final ValueChanged? onLongPressEnd; + final VoidCallback? onDoubleTap; + final ValueChanged? onDoubleTapDown; + final VoidCallback? onDoubleTapCancel; + final Duration? longPressDuration; + + @override + State> createState() => _ShadTabState(); + + @override + Size get preferredSize { + return Size.fromHeight(height ?? 32); + } +} + +class _ShadTabState extends State> { + FocusNode? _focusNode; + + FocusNode get focusNode => widget.focusNode ?? _focusNode!; + + late bool focused = focusNode.hasFocus; + + @override + void initState() { + super.initState(); + if (widget.focusNode == null) _focusNode = FocusNode(); + focusNode.addListener(focusNodeListener); + } + + @override + void didUpdateWidget(covariant ShadTab oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.focusNode == null && oldWidget.focusNode != null) { + focusNode.removeListener(focusNodeListener); + _focusNode = FocusNode(); + focusNode.addListener(focusNodeListener); + } else if (widget.focusNode != null && oldWidget.focusNode == null) { + focusNode.removeListener(focusNodeListener); + _focusNode?.dispose(); + _focusNode = null; + focusNode.addListener(focusNodeListener); + } + } + + @override + void dispose() { + _focusNode?.dispose(); + super.dispose(); + } + + void focusNodeListener() { + setState(() => focused = focusNode.hasFocus); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final inherited = ShadTabsInheritedWidget.of(context); + + final tabsTheme = theme.tabsTheme; + + final defaultWidth = inherited.scrollable ? null : double.infinity; + final effectiveWidth = widget.width ?? tabsTheme.tabWidth ?? defaultWidth; + final effectiveBackgroundColor = widget.backgroundColor ?? + tabsTheme.tabBackgroundColor ?? + Colors.transparent; + final effectiveSelectedBackgroundColor = widget.selectedBackgroundColor ?? + tabsTheme.tabSelectedBackgroundColor ?? + theme.colorScheme.background; + + final effectiveHoverBackgroundColor = widget.hoverBackgroundColor ?? + tabsTheme.tabHoverForegroundColor ?? + effectiveBackgroundColor; + + final effectiveSelectedHoverBackgroundColor = + widget.selectedHoverBackgroundColor ?? + tabsTheme.tabSelectedHoverBackgroundColor ?? + effectiveSelectedBackgroundColor; + + final effectivePadding = widget.padding ?? + tabsTheme.tabPadding ?? + const EdgeInsets.symmetric(horizontal: 12, vertical: 6); + + final effectiveForegroundColor = widget.foregroundColor ?? + tabsTheme.tabForegroundColor ?? + theme.colorScheme.foreground; + + final effectiveSelectedForegroundColor = widget.selectedForegroundColor ?? + tabsTheme.tabSelectedForegroundColor ?? + effectiveForegroundColor; + + final effectiveShadows = widget.shadows ?? tabsTheme.tabShadows; + final effectiveSelectedShadows = widget.selectedShadows ?? + tabsTheme.tabSelectedShadows ?? + ShadShadows.sm; + + final effectiveMainAxisAlignment = + widget.mainAxisAlignment ?? tabsTheme.tabMainAxisAlignment; + final effectiveCrossAxisAlignment = + widget.crossAxisAlignment ?? tabsTheme.tabCrossAxisAlignment; + final effectiveHoverStrategies = + widget.hoverStrategies ?? tabsTheme.tabHoverStrategies; + + final effectiveTextDecoration = + widget.textDecoration ?? tabsTheme.tabTextDecoration; + final effectiveHoverTextDecoration = + widget.hoverTextDecoration ?? tabsTheme.tabHoverTextDecoration; + final effectiveGradient = widget.gradient ?? tabsTheme.tabGradient; + final effectiveSize = widget.size ?? tabsTheme.tabSize; + final effectiveHoverForegroundColor = + widget.hoverForegroundColor ?? tabsTheme.tabHoverForegroundColor; + final effectiveCursor = widget.cursor ?? tabsTheme.tabCursor; + final effectiveApplyIconColorFilter = + widget.applyIconColorFilter ?? tabsTheme.tabApplyIconColorFilter; + final effectivePressedBackgroundColor = + widget.pressedBackgroundColor ?? tabsTheme.tabPressedBackgroundColor; + final effectivePressedForegroundColor = + widget.pressedForegroundColor ?? tabsTheme.tabPressedForegroundColor; + + Widget tab = ListenableBuilder( + listenable: inherited.controller, + builder: (context, _) { + final selected = inherited.controller.selected == widget.value; + final isFirstTab = inherited.orderedValues.first == widget.value; + final isLastTab = inherited.orderedValues.last == widget.value; + + final effectiveDecoration = widget.decoration ?? + tabsTheme.tabDecoration ?? + ShadDecoration( + border: ShadBorder(radius: BorderRadius.circular(2)), + secondaryBorder: ShadBorder( + radius: BorderRadius.circular(2), + padding: EdgeInsets.fromLTRB( + isFirstTab ? 4 : 2, + 4, + isLastTab ? 4 : 2, + 4, + ), + ), + secondaryFocusedBorder: ShadBorder( + radius: theme.radius, + padding: EdgeInsets.fromLTRB( + isFirstTab ? 2 : 0, + 2, + isLastTab ? 2 : 0, + 2, + ), + ), + ); + + return ShadButton.secondary( + icon: widget.icon, + focusNode: focusNode, + height: widget.preferredSize.height, + width: effectiveWidth, + backgroundColor: selected + ? effectiveSelectedBackgroundColor + : effectiveBackgroundColor, + hoverBackgroundColor: selected + ? effectiveSelectedHoverBackgroundColor + : effectiveHoverBackgroundColor, + padding: effectivePadding, + decoration: effectiveDecoration, + foregroundColor: selected + ? effectiveSelectedForegroundColor + : effectiveForegroundColor, + text: DefaultTextStyle( + style: theme.textTheme.small, + child: widget.text, + ), + shadows: selected ? effectiveSelectedShadows : effectiveShadows, + onPressed: () { + inherited.controller.select(widget.value); + widget.onPressed?.call(); + }, + enabled: widget.enabled, + onLongPress: widget.onLongPress, + size: effectiveSize, + applyIconColorFilter: effectiveApplyIconColorFilter, + cursor: effectiveCursor, + hoverForegroundColor: effectiveHoverForegroundColor, + autofocus: widget.autofocus, + pressedBackgroundColor: effectivePressedBackgroundColor, + pressedForegroundColor: effectivePressedForegroundColor, + gradient: effectiveGradient, + textDecoration: effectiveTextDecoration, + hoverTextDecoration: effectiveHoverTextDecoration, + statesController: widget.statesController, + mainAxisAlignment: effectiveMainAxisAlignment, + crossAxisAlignment: effectiveCrossAxisAlignment, + hoverStrategies: effectiveHoverStrategies, + onHoverChange: widget.onHoverChange, + onTapDown: widget.onTapDown, + onTapUp: widget.onTapUp, + onTapCancel: widget.onTapCancel, + onLongPressStart: widget.onLongPressStart, + onLongPressCancel: widget.onLongPressCancel, + onLongPressUp: widget.onLongPressUp, + onLongPressDown: widget.onLongPressDown, + onLongPressEnd: widget.onLongPressEnd, + onDoubleTap: widget.onDoubleTap, + onDoubleTapDown: widget.onDoubleTapDown, + onDoubleTapCancel: widget.onDoubleTapCancel, + longPressDuration: widget.longPressDuration, + ); + }, + ); + + if (!inherited.scrollable) { + tab = Expanded(flex: widget.flex, child: tab); + } + + return tab; + } +} diff --git a/lib/src/theme/color_scheme/base.dart b/lib/src/theme/color_scheme/base.dart index 998ed88f..192688fd 100644 --- a/lib/src/theme/color_scheme/base.dart +++ b/lib/src/theme/color_scheme/base.dart @@ -1,4 +1,6 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; @immutable class ShadColorScheme { @@ -25,6 +27,51 @@ class ShadColorScheme { required this.selection, }); + factory ShadColorScheme.fromName( + String name, { + Brightness brightness = Brightness.light, + }) { + return switch (name) { + 'blue' => brightness == Brightness.light + ? const ShadBlueColorScheme.light() + : const ShadBlueColorScheme.dark(), + 'gray' => brightness == Brightness.light + ? const ShadGrayColorScheme.light() + : const ShadGrayColorScheme.dark(), + 'green' => brightness == Brightness.light + ? const ShadGreenColorScheme.light() + : const ShadGreenColorScheme.dark(), + 'neutral' => brightness == Brightness.light + ? const ShadNeutralColorScheme.light() + : const ShadNeutralColorScheme.dark(), + 'orange' => brightness == Brightness.light + ? const ShadOrangeColorScheme.light() + : const ShadOrangeColorScheme.dark(), + 'red' => brightness == Brightness.light + ? const ShadRedColorScheme.light() + : const ShadRedColorScheme.dark(), + 'rose' => brightness == Brightness.light + ? const ShadRoseColorScheme.light() + : const ShadRoseColorScheme.dark(), + 'slate' => brightness == Brightness.light + ? const ShadSlateColorScheme.light() + : const ShadSlateColorScheme.dark(), + 'stone' => brightness == Brightness.light + ? const ShadStoneColorScheme.light() + : const ShadStoneColorScheme.dark(), + 'violet' => brightness == Brightness.light + ? const ShadVioletColorScheme.light() + : const ShadVioletColorScheme.dark(), + 'yellow' => brightness == Brightness.light + ? const ShadYellowColorScheme.light() + : const ShadYellowColorScheme.dark(), + 'zinc' => brightness == Brightness.light + ? const ShadZincColorScheme.light() + : const ShadZincColorScheme.dark(), + _ => throw Exception('Invalid color scheme name'), + }; + } + final Color background; final Color foreground; final Color card; diff --git a/lib/src/theme/components/tabs.dart b/lib/src/theme/components/tabs.dart new file mode 100644 index 00000000..290a183d --- /dev/null +++ b/lib/src/theme/components/tabs.dart @@ -0,0 +1,450 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:shadcn_ui/src/components/button.dart'; +import 'package:shadcn_ui/src/theme/components/decorator.dart'; +import 'package:shadcn_ui/src/utils/gesture_detector.dart'; + +@immutable +class ShadTabsTheme { + const ShadTabsTheme({ + this.merge = true, + this.gap, + this.dragStartBehavior, + this.physics, + this.padding, + this.decoration, + this.tabBarConstraints, + this.contentConstraints, + this.expandContent, + this.tabWidth, + this.tabBackgroundColor, + this.tabSelectedBackgroundColor, + this.tabHoverBackgroundColor, + this.tabSelectedHoverBackgroundColor, + this.tabPadding, + this.tabDecoration, + this.tabSelectedDecoration, + this.tabForegroundColor, + this.tabSelectedForegroundColor, + this.tabTextStyle, + this.tabShadows, + this.tabSelectedShadows, + this.tabSize, + this.tabApplyIconColorFilter, + this.tabCursor, + this.tabHoverForegroundColor, + this.tabPressedBackgroundColor, + this.tabPressedForegroundColor, + this.tabGradient, + this.tabTextDecoration, + this.tabHoverTextDecoration, + this.tabMainAxisAlignment, + this.tabCrossAxisAlignment, + this.tabHoverStrategies, + this.longPressDuration, + }); + + final bool merge; + + /// {@macro ShadTabsTheme.gap} + final double? gap; + + /// {@macro ShadTabs.dragStartBehavior} + final DragStartBehavior? dragStartBehavior; + + /// {@macro ShadTabs.physics} + final ScrollPhysics? physics; + + /// {@macro ShadTabs.padding} + final EdgeInsets? padding; + + /// {@macro ShadTabs.decoration} + final ShadDecoration? decoration; + + /// {@macro ShadTabs.tabBarConstraints} + final BoxConstraints? tabBarConstraints; + + /// {@macro ShadTabs.contentConstraints} + final BoxConstraints? contentConstraints; + + /// {@macro ShadTabs.expandContent} + final bool? expandContent; + + /// {@macro ShadTabs.tabWidth} + final double? tabWidth; + + /// {@macro ShadTabs.tabBackgroundColor} + final Color? tabBackgroundColor; + + /// {@macro ShadTabs.tabSelectedBackgroundColor} + final Color? tabSelectedBackgroundColor; + + /// {@macro ShadTabs.tabHoverBackgroundColor} + final Color? tabHoverBackgroundColor; + + /// {@macro ShadTabs.tabSelectedHoverBackgroundColor} + final Color? tabSelectedHoverBackgroundColor; + + /// {@macro ShadTabs.tabPadding} + final EdgeInsets? tabPadding; + + /// {@macro ShadTabs.tabDecoration} + final ShadDecoration? tabDecoration; + + /// {@macro ShadTabs.tabSelectedDecoration} + final ShadDecoration? tabSelectedDecoration; + + /// {@macro ShadTabs.tabForegroundColor} + final Color? tabForegroundColor; + + /// {@macro ShadTabs.tabSelectedForegroundColor} + final Color? tabSelectedForegroundColor; + + /// {@macro ShadTabs.tabTextStyle} + final TextStyle? tabTextStyle; + + /// {@macro ShadTabs.tabShadows} + final List? tabShadows; + + /// {@macro ShadTabs.tabSelectedShadows} + final List? tabSelectedShadows; + + /// {@macro ShadTabs.tabSize} + final ShadButtonSize? tabSize; + + /// {@macro ShadTabs.tabApplyIconColorFilter} + final bool? tabApplyIconColorFilter; + + /// {@macro ShadTabs.tabCursor} + final MouseCursor? tabCursor; + + /// {@macro ShadTabs.tabHoverForegroundColor} + final Color? tabHoverForegroundColor; + + /// {@macro ShadTabs.tabPressedBackgroundColor} + final Color? tabPressedBackgroundColor; + + /// {@macro ShadTabs.tabPressedForegroundColor} + final Color? tabPressedForegroundColor; + + /// {@macro ShadTabs.tabGradient} + final Gradient? tabGradient; + + /// {@macro ShadTabs.tabTextDecoration} + final TextDecoration? tabTextDecoration; + + /// {@macro ShadTabs.tabHoverTextDecoration} + final TextDecoration? tabHoverTextDecoration; + + /// {@macro ShadButton.tabMainAxisAlignment} + final MainAxisAlignment? tabMainAxisAlignment; + + /// {@macro ShadButton.crossAxisAlignment} + final CrossAxisAlignment? tabCrossAxisAlignment; + + /// {@macro ShadButton.tabHoverStrategies} + final ShadHoverStrategies? tabHoverStrategies; + + /// {@macro ShadButton.longPressDuration} + final Duration? longPressDuration; + + static ShadTabsTheme lerp( + ShadTabsTheme a, + ShadTabsTheme b, + double t, + ) { + if (identical(a, b)) return a; + return ShadTabsTheme( + merge: t < 0.5 ? a.merge : b.merge, + gap: lerpDouble(a.gap, b.gap, t), + dragStartBehavior: t < 0.5 ? a.dragStartBehavior : b.dragStartBehavior, + physics: t < 0.5 ? a.physics : b.physics, + padding: EdgeInsets.lerp(a.padding, b.padding, t), + decoration: ShadDecoration.lerp(a.decoration, b.decoration, t), + tabBarConstraints: + BoxConstraints.lerp(a.tabBarConstraints, b.tabBarConstraints, t), + contentConstraints: + BoxConstraints.lerp(a.contentConstraints, b.contentConstraints, t), + expandContent: t < 0.5 ? a.expandContent : b.expandContent, + tabWidth: lerpDouble(a.tabWidth, b.tabWidth, t), + tabBackgroundColor: + Color.lerp(a.tabBackgroundColor, b.tabBackgroundColor, t), + tabSelectedBackgroundColor: Color.lerp( + a.tabSelectedBackgroundColor, + b.tabSelectedBackgroundColor, + t, + ), + tabHoverBackgroundColor: + Color.lerp(a.tabHoverBackgroundColor, b.tabHoverBackgroundColor, t), + tabSelectedHoverBackgroundColor: Color.lerp( + a.tabSelectedHoverBackgroundColor, + b.tabSelectedHoverBackgroundColor, + t, + ), + tabPadding: EdgeInsets.lerp(a.tabPadding, b.tabPadding, t), + tabDecoration: ShadDecoration.lerp(a.tabDecoration, b.tabDecoration, t), + tabSelectedDecoration: ShadDecoration.lerp( + a.tabSelectedDecoration, + b.tabSelectedDecoration, + t, + ), + tabForegroundColor: Color.lerp( + a.tabForegroundColor, + b.tabForegroundColor, + t, + ), + tabSelectedForegroundColor: Color.lerp( + a.tabSelectedForegroundColor, + b.tabSelectedForegroundColor, + t, + ), + tabTextStyle: TextStyle.lerp(a.tabTextStyle, b.tabTextStyle, t), + tabShadows: BoxShadow.lerpList(a.tabShadows, b.tabShadows, t), + tabSelectedShadows: + BoxShadow.lerpList(a.tabSelectedShadows, b.tabSelectedShadows, t), + tabSize: t < 0.5 ? a.tabSize : b.tabSize, + tabApplyIconColorFilter: + t < 0.5 ? a.tabApplyIconColorFilter : b.tabApplyIconColorFilter, + tabCursor: t < 0.5 ? a.tabCursor : b.tabCursor, + tabHoverForegroundColor: Color.lerp( + a.tabHoverForegroundColor, + b.tabHoverForegroundColor, + t, + ), + tabPressedBackgroundColor: Color.lerp( + a.tabPressedBackgroundColor, + b.tabPressedBackgroundColor, + t, + ), + tabPressedForegroundColor: Color.lerp( + a.tabPressedForegroundColor, + b.tabPressedForegroundColor, + t, + ), + tabGradient: Gradient.lerp(a.tabGradient, b.tabGradient, t), + tabTextDecoration: t < 0.5 ? a.tabTextDecoration : b.tabTextDecoration, + tabHoverTextDecoration: + t < 0.5 ? a.tabHoverTextDecoration : b.tabHoverTextDecoration, + tabMainAxisAlignment: + t < 0.5 ? a.tabMainAxisAlignment : b.tabMainAxisAlignment, + tabCrossAxisAlignment: + t < 0.5 ? a.tabCrossAxisAlignment : b.tabCrossAxisAlignment, + tabHoverStrategies: t < 0.5 ? a.tabHoverStrategies : b.tabHoverStrategies, + longPressDuration: t < 0.5 ? a.longPressDuration : b.longPressDuration, + ); + } + + ShadTabsTheme mergeWith(ShadTabsTheme? other) { + if (other == null) return this; + if (!other.merge) return other; + return copyWith( + gap: other.gap, + dragStartBehavior: other.dragStartBehavior, + physics: other.physics, + padding: other.padding, + decoration: other.decoration, + tabBarConstraints: other.tabBarConstraints, + contentConstraints: other.contentConstraints, + expandContent: other.expandContent, + tabWidth: other.tabWidth, + tabBackgroundColor: other.tabBackgroundColor, + tabSelectedBackgroundColor: other.tabSelectedBackgroundColor, + tabHoverBackgroundColor: other.tabHoverBackgroundColor, + tabSelectedHoverBackgroundColor: other.tabSelectedHoverBackgroundColor, + tabPadding: other.tabPadding, + tabDecoration: other.tabDecoration, + tabSelectedDecoration: other.tabSelectedDecoration, + tabForegroundColor: other.tabForegroundColor, + tabSelectedForegroundColor: other.tabSelectedForegroundColor, + tabTextStyle: other.tabTextStyle, + tabShadows: other.tabShadows, + tabSelectedShadows: other.tabSelectedShadows, + tabSize: other.tabSize, + tabApplyIconColorFilter: other.tabApplyIconColorFilter, + tabCursor: other.tabCursor, + tabHoverForegroundColor: other.tabHoverForegroundColor, + tabPressedBackgroundColor: other.tabPressedBackgroundColor, + tabPressedForegroundColor: other.tabPressedForegroundColor, + tabGradient: other.tabGradient, + tabTextDecoration: other.tabTextDecoration, + tabHoverTextDecoration: other.tabHoverTextDecoration, + tabMainAxisAlignment: other.tabMainAxisAlignment, + tabCrossAxisAlignment: other.tabCrossAxisAlignment, + tabHoverStrategies: other.tabHoverStrategies, + longPressDuration: other.longPressDuration, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ShadTabsTheme && + other.merge == merge && + other.gap == gap && + other.dragStartBehavior == dragStartBehavior && + other.physics == physics && + other.padding == padding && + other.decoration == decoration && + other.tabBarConstraints == tabBarConstraints && + other.contentConstraints == contentConstraints && + other.expandContent == expandContent && + other.tabWidth == tabWidth && + other.tabBackgroundColor == tabBackgroundColor && + other.tabSelectedBackgroundColor == tabSelectedBackgroundColor && + other.tabHoverBackgroundColor == tabHoverBackgroundColor && + other.tabSelectedHoverBackgroundColor == + tabSelectedHoverBackgroundColor && + other.tabPadding == tabPadding && + other.tabDecoration == tabDecoration && + other.tabSelectedDecoration == tabSelectedDecoration && + other.tabForegroundColor == tabForegroundColor && + other.tabSelectedForegroundColor == tabSelectedForegroundColor && + other.tabTextStyle == tabTextStyle && + listEquals(other.tabShadows, tabShadows) && + listEquals(other.tabSelectedShadows, tabSelectedShadows) && + other.tabSize == tabSize && + other.tabApplyIconColorFilter == tabApplyIconColorFilter && + other.tabCursor == tabCursor && + other.tabHoverForegroundColor == tabHoverForegroundColor && + other.tabPressedBackgroundColor == tabPressedBackgroundColor && + other.tabPressedForegroundColor == tabPressedForegroundColor && + other.tabGradient == tabGradient && + other.tabTextDecoration == tabTextDecoration && + other.tabHoverTextDecoration == tabHoverTextDecoration && + other.tabMainAxisAlignment == tabMainAxisAlignment && + other.tabCrossAxisAlignment == tabCrossAxisAlignment && + other.tabHoverStrategies == tabHoverStrategies && + other.longPressDuration == longPressDuration; + } + + @override + int get hashCode { + return merge.hashCode ^ + gap.hashCode ^ + dragStartBehavior.hashCode ^ + physics.hashCode ^ + padding.hashCode ^ + decoration.hashCode ^ + tabBarConstraints.hashCode ^ + contentConstraints.hashCode ^ + expandContent.hashCode ^ + tabWidth.hashCode ^ + tabBackgroundColor.hashCode ^ + tabSelectedBackgroundColor.hashCode ^ + tabHoverBackgroundColor.hashCode ^ + tabSelectedHoverBackgroundColor.hashCode ^ + tabPadding.hashCode ^ + tabDecoration.hashCode ^ + tabSelectedDecoration.hashCode ^ + tabForegroundColor.hashCode ^ + tabSelectedForegroundColor.hashCode ^ + tabTextStyle.hashCode ^ + tabShadows.hashCode ^ + tabSelectedShadows.hashCode ^ + tabSize.hashCode ^ + tabApplyIconColorFilter.hashCode ^ + tabCursor.hashCode ^ + tabHoverForegroundColor.hashCode ^ + tabPressedBackgroundColor.hashCode ^ + tabPressedForegroundColor.hashCode ^ + tabGradient.hashCode ^ + tabTextDecoration.hashCode ^ + tabHoverTextDecoration.hashCode ^ + tabMainAxisAlignment.hashCode ^ + tabCrossAxisAlignment.hashCode ^ + tabHoverStrategies.hashCode ^ + longPressDuration.hashCode; + } + + ShadTabsTheme copyWith({ + bool? merge, + double? gap, + DragStartBehavior? dragStartBehavior, + ScrollPhysics? physics, + EdgeInsets? padding, + ShadDecoration? decoration, + BoxConstraints? tabBarConstraints, + BoxConstraints? contentConstraints, + bool? expandContent, + double? tabWidth, + Color? tabBackgroundColor, + Color? tabSelectedBackgroundColor, + Color? tabHoverBackgroundColor, + Color? tabSelectedHoverBackgroundColor, + EdgeInsets? tabPadding, + ShadDecoration? tabDecoration, + ShadDecoration? tabSelectedDecoration, + Color? tabForegroundColor, + Color? tabSelectedForegroundColor, + TextStyle? tabTextStyle, + List? tabShadows, + List? tabSelectedShadows, + ShadButtonSize? tabSize, + bool? tabApplyIconColorFilter, + MouseCursor? tabCursor, + Color? tabHoverForegroundColor, + Color? tabPressedBackgroundColor, + Color? tabPressedForegroundColor, + Gradient? tabGradient, + TextDecoration? tabTextDecoration, + TextDecoration? tabHoverTextDecoration, + MainAxisAlignment? tabMainAxisAlignment, + CrossAxisAlignment? tabCrossAxisAlignment, + ShadHoverStrategies? tabHoverStrategies, + Duration? longPressDuration, + }) { + return ShadTabsTheme( + merge: merge ?? this.merge, + gap: gap ?? this.gap, + dragStartBehavior: dragStartBehavior ?? this.dragStartBehavior, + physics: physics ?? this.physics, + padding: padding ?? this.padding, + decoration: decoration ?? this.decoration, + tabBarConstraints: tabBarConstraints ?? this.tabBarConstraints, + contentConstraints: contentConstraints ?? this.contentConstraints, + expandContent: expandContent ?? this.expandContent, + tabWidth: tabWidth ?? this.tabWidth, + tabBackgroundColor: tabBackgroundColor ?? this.tabBackgroundColor, + tabSelectedBackgroundColor: + tabSelectedBackgroundColor ?? this.tabSelectedBackgroundColor, + tabHoverBackgroundColor: + tabHoverBackgroundColor ?? this.tabHoverBackgroundColor, + tabSelectedHoverBackgroundColor: tabSelectedHoverBackgroundColor ?? + this.tabSelectedHoverBackgroundColor, + tabPadding: tabPadding ?? this.tabPadding, + tabDecoration: tabDecoration ?? this.tabDecoration, + tabSelectedDecoration: + tabSelectedDecoration ?? this.tabSelectedDecoration, + tabForegroundColor: tabForegroundColor ?? this.tabForegroundColor, + tabSelectedForegroundColor: + tabSelectedForegroundColor ?? this.tabSelectedForegroundColor, + tabTextStyle: tabTextStyle ?? this.tabTextStyle, + tabShadows: tabShadows ?? this.tabShadows, + tabSelectedShadows: tabSelectedShadows ?? this.tabSelectedShadows, + tabSize: tabSize ?? this.tabSize, + tabApplyIconColorFilter: + tabApplyIconColorFilter ?? this.tabApplyIconColorFilter, + tabCursor: tabCursor ?? this.tabCursor, + tabHoverForegroundColor: + tabHoverForegroundColor ?? this.tabHoverForegroundColor, + tabPressedBackgroundColor: + tabPressedBackgroundColor ?? this.tabPressedBackgroundColor, + tabPressedForegroundColor: + tabPressedForegroundColor ?? this.tabPressedForegroundColor, + tabGradient: tabGradient ?? this.tabGradient, + tabTextDecoration: tabTextDecoration ?? this.tabTextDecoration, + tabHoverTextDecoration: + tabHoverTextDecoration ?? this.tabHoverTextDecoration, + tabMainAxisAlignment: tabMainAxisAlignment ?? this.tabMainAxisAlignment, + tabCrossAxisAlignment: + tabCrossAxisAlignment ?? this.tabCrossAxisAlignment, + tabHoverStrategies: tabHoverStrategies ?? this.tabHoverStrategies, + longPressDuration: longPressDuration ?? this.longPressDuration, + ); + } +} diff --git a/lib/src/theme/data.dart b/lib/src/theme/data.dart index 99c7fdae..f353be57 100644 --- a/lib/src/theme/data.dart +++ b/lib/src/theme/data.dart @@ -22,6 +22,7 @@ import 'package:shadcn_ui/src/theme/components/sheet.dart'; import 'package:shadcn_ui/src/theme/components/slider.dart'; import 'package:shadcn_ui/src/theme/components/switch.dart'; import 'package:shadcn_ui/src/theme/components/table.dart'; +import 'package:shadcn_ui/src/theme/components/tabs.dart'; import 'package:shadcn_ui/src/theme/components/toast.dart'; import 'package:shadcn_ui/src/theme/components/tooltip.dart'; import 'package:shadcn_ui/src/theme/text_theme/theme.dart'; @@ -76,6 +77,7 @@ class ShadThemeData extends ShadBaseTheme { ShadResizableTheme? resizableTheme, ShadHoverStrategies? hoverStrategies, bool? disableSecondaryBorder, + ShadTabsTheme? tabsTheme, }) { final effectiveRadius = radius ?? const BorderRadius.all(Radius.circular(6)); @@ -267,6 +269,10 @@ class ShadThemeData extends ShadBaseTheme { hoverStrategies: hoverStrategies ?? ShadDefaultComponentThemes.hoverStrategies(), disableSecondaryBorder: effectiveDisableSecondaryBorder, + tabsTheme: ShadDefaultComponentThemes.tabsTheme( + colorScheme: colorScheme, + radius: effectiveRadius, + ).mergeWith(tabsTheme), ); } @@ -314,6 +320,7 @@ class ShadThemeData extends ShadBaseTheme { required super.resizableTheme, required super.hoverStrategies, required super.disableSecondaryBorder, + required super.tabsTheme, }); static ShadThemeData lerp(ShadThemeData a, ShadThemeData b, double t) { @@ -405,6 +412,7 @@ class ShadThemeData extends ShadBaseTheme { hoverStrategies: t < .5 ? a.hoverStrategies : b.hoverStrategies, disableSecondaryBorder: t < .5 ? a.disableSecondaryBorder : b.disableSecondaryBorder, + tabsTheme: ShadTabsTheme.lerp(a.tabsTheme, b.tabsTheme, t), ); } @@ -455,7 +463,8 @@ class ShadThemeData extends ShadBaseTheme { other.tableTheme == tableTheme && other.resizableTheme == resizableTheme && other.hoverStrategies == hoverStrategies && - other.disableSecondaryBorder == disableSecondaryBorder; + other.disableSecondaryBorder == disableSecondaryBorder && + other.tabsTheme == tabsTheme; } @override @@ -502,7 +511,8 @@ class ShadThemeData extends ShadBaseTheme { tableTheme.hashCode ^ resizableTheme.hashCode ^ hoverStrategies.hashCode ^ - disableSecondaryBorder.hashCode; + disableSecondaryBorder.hashCode ^ + tabsTheme.hashCode; } ShadThemeData copyWith({ @@ -549,6 +559,7 @@ class ShadThemeData extends ShadBaseTheme { ShadResizableTheme? resizableTheme, ShadHoverStrategies? hoverStrategies, bool? disableSecondaryBorder, + ShadTabsTheme? tabsTheme, }) { return ShadThemeData( colorScheme: colorScheme ?? this.colorScheme, @@ -599,6 +610,7 @@ class ShadThemeData extends ShadBaseTheme { hoverStrategies: hoverStrategies ?? this.hoverStrategies, disableSecondaryBorder: disableSecondaryBorder ?? this.disableSecondaryBorder, + tabsTheme: tabsTheme ?? this.tabsTheme, ); } } diff --git a/lib/src/theme/themes/base.dart b/lib/src/theme/themes/base.dart index 852782a2..ad9fcfff 100644 --- a/lib/src/theme/themes/base.dart +++ b/lib/src/theme/themes/base.dart @@ -20,6 +20,7 @@ import 'package:shadcn_ui/src/theme/components/sheet.dart'; import 'package:shadcn_ui/src/theme/components/slider.dart'; import 'package:shadcn_ui/src/theme/components/switch.dart'; import 'package:shadcn_ui/src/theme/components/table.dart'; +import 'package:shadcn_ui/src/theme/components/tabs.dart'; import 'package:shadcn_ui/src/theme/components/toast.dart'; import 'package:shadcn_ui/src/theme/components/tooltip.dart'; import 'package:shadcn_ui/src/theme/text_theme/theme.dart'; @@ -72,6 +73,7 @@ abstract class ShadBaseTheme { required this.resizableTheme, required this.hoverStrategies, required this.disableSecondaryBorder, + required this.tabsTheme, }); final ShadColorScheme colorScheme; @@ -117,4 +119,5 @@ abstract class ShadBaseTheme { final ShadResizableTheme resizableTheme; final ShadHoverStrategies hoverStrategies; final bool disableSecondaryBorder; + final ShadTabsTheme tabsTheme; } diff --git a/lib/src/theme/themes/component_defaults.dart b/lib/src/theme/themes/component_defaults.dart index 2d0551c0..5449e0cf 100644 --- a/lib/src/theme/themes/component_defaults.dart +++ b/lib/src/theme/themes/component_defaults.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -25,6 +24,7 @@ import 'package:shadcn_ui/src/theme/components/sheet.dart'; import 'package:shadcn_ui/src/theme/components/slider.dart'; import 'package:shadcn_ui/src/theme/components/switch.dart'; import 'package:shadcn_ui/src/theme/components/table.dart'; +import 'package:shadcn_ui/src/theme/components/tabs.dart'; import 'package:shadcn_ui/src/theme/components/toast.dart'; import 'package:shadcn_ui/src/theme/components/tooltip.dart'; import 'package:shadcn_ui/src/theme/text_theme/text_styles_default.dart'; @@ -710,4 +710,29 @@ abstract class ShadDefaultComponentThemes { }, ); } + + static ShadTabsTheme tabsTheme({ + required ShadColorScheme colorScheme, + required BorderRadius radius, + }) { + return ShadTabsTheme( + dragStartBehavior: DragStartBehavior.start, + padding: EdgeInsets.zero, + decoration: ShadDecoration( + merge: false, + color: colorScheme.muted, + border: ShadBorder(radius: radius), + ), + gap: 8, + expandContent: false, + tabBackgroundColor: Colors.transparent, + tabSelectedBackgroundColor: colorScheme.background, + tabHoverBackgroundColor: Colors.transparent, + tabSelectedHoverBackgroundColor: colorScheme.background, + tabPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + tabForegroundColor: colorScheme.foreground, + tabSelectedForegroundColor: colorScheme.foreground, + tabSelectedShadows: ShadShadows.sm, + ); + } } diff --git a/playground/lib/main.dart b/playground/lib/main.dart index 170d8ec5..505ac240 100644 --- a/playground/lib/main.dart +++ b/playground/lib/main.dart @@ -20,6 +20,7 @@ import 'package:playground/pages/sheet.dart'; import 'package:playground/pages/slider.dart'; import 'package:playground/pages/switch.dart'; import 'package:playground/pages/table.dart'; +import 'package:playground/pages/tabs.dart'; import 'package:playground/pages/toast.dart'; import 'package:playground/pages/tooltip.dart'; import 'package:playground/pages/typography.dart'; @@ -230,5 +231,9 @@ final _router = GoRouter( ); }, ), + GoRoute( + path: '/tabs', + builder: (context, state) => const TabsPage(), + ), ], ); diff --git a/playground/lib/pages/tabs.dart b/playground/lib/pages/tabs.dart new file mode 100644 index 00000000..18aa8b9a --- /dev/null +++ b/playground/lib/pages/tabs.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +import 'package:shadcn_ui/shadcn_ui.dart'; + +class TabsPage extends StatelessWidget { + const TabsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: ShadTabs( + defaultValue: 'account', + tabBarConstraints: const BoxConstraints(maxWidth: 400), + contentConstraints: const BoxConstraints(maxWidth: 400), + tabs: [ + ShadTab( + value: 'account', + text: const Text('Account'), + content: ShadCard( + title: const Text('Account'), + description: const Text( + "Make changes to your account here. Click save when you're done."), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Name'), + initialValue: 'Ale', + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('Username'), + initialValue: 'nank1ro', + ), + const SizedBox(height: 16), + ], + ), + footer: const ShadButton(text: Text('Save changes')), + ), + ), + ShadTab( + value: 'password', + text: const Text('Password'), + content: ShadCard( + title: const Text('Password'), + description: const Text( + "Change your password here. After saving, you'll be logged out."), + content: Column( + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Current password'), + obscureText: true, + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('New password'), + obscureText: true, + ), + const SizedBox(height: 16), + ], + ), + footer: const ShadButton(text: Text('Save password')), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index cd1e63d6..8165f468 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shadcn_ui description: shadcn-ui ported in Flutter. Awesome UI components for Flutter, fully customizable. -version: 0.5.7 +version: 0.6.0 homepage: https://mariuti.com/shadcn-ui repository: https://github.com/nank1ro/flutter-shadcn-ui documentation: https://mariuti.com/shadcn-ui