diff --git a/catalyst_voices/lib/pages/spaces/my_private_proposals.dart b/catalyst_voices/lib/pages/spaces/my_private_proposals.dart new file mode 100644 index 00000000000..9bd7017c46c --- /dev/null +++ b/catalyst_voices/lib/pages/spaces/my_private_proposals.dart @@ -0,0 +1,32 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class MyPrivateProposals extends StatelessWidget { + const MyPrivateProposals({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SectionHeader( + leading: SizedBox(width: 12), + title: Text('My private proposals (3/5)'), + ), + VoicesDrawerNavItem( + name: 'My first proposal', + status: ProposalStatus.draft, + ), + VoicesDrawerNavItem( + name: 'My second proposal', + status: ProposalStatus.inProgress, + ), + VoicesDrawerNavItem( + name: 'My third proposal', + status: ProposalStatus.inProgress, + ), + ], + ); + } +} diff --git a/catalyst_voices/lib/pages/spaces/spaces_drawer.dart b/catalyst_voices/lib/pages/spaces/spaces_drawer.dart index 55c7b9a8083..202544a0cb6 100644 --- a/catalyst_voices/lib/pages/spaces/spaces_drawer.dart +++ b/catalyst_voices/lib/pages/spaces/spaces_drawer.dart @@ -1,4 +1,5 @@ import 'package:catalyst_voices/pages/spaces/individual_private_campaigns.dart'; +import 'package:catalyst_voices/pages/spaces/my_private_proposals.dart'; import 'package:catalyst_voices/routes/routes.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; @@ -18,7 +19,7 @@ class SpacesDrawer extends StatelessWidget { return VoicesDrawer( children: [ _SpaceHeader(space), - if (space == Space.treasury) IndividualPrivateCampaigns(), + _space(), ], bottom: VoicesDrawerSpaceChooser( currentSpace: space, @@ -30,6 +31,17 @@ class SpacesDrawer extends StatelessWidget { ); } + Widget _space() { + switch (space) { + case Space.treasury: + return IndividualPrivateCampaigns(); + case Space.workspace: + return MyPrivateProposals(); + default: + return SizedBox(); + } + } + void _goTo( BuildContext context, { required Space space, diff --git a/catalyst_voices/lib/pages/treasury/campaign_details.dart b/catalyst_voices/lib/pages/treasury/campaign_details.dart index 45af95ec79c..674c14004dd 100644 --- a/catalyst_voices/lib/pages/treasury/campaign_details.dart +++ b/catalyst_voices/lib/pages/treasury/campaign_details.dart @@ -129,16 +129,16 @@ class _StepDetails extends StatelessWidget { @override Widget build(BuildContext context) { - return WorkspaceTileContainer( + return WorkspaceTextTileContainer( name: name, isSelected: isSelected, headerActions: [ VoicesTextButton( - child: Text(context.l10n.treasuryStepEdit), + child: Text(context.l10n.stepEdit), onTap: isEditable ? () {} : null, ), ], - content: Text(desc), + content: desc, ); } } diff --git a/catalyst_voices/lib/pages/workspace/proposal_details.dart b/catalyst_voices/lib/pages/workspace/proposal_details.dart new file mode 100644 index 00000000000..f3fb82bb4f9 --- /dev/null +++ b/catalyst_voices/lib/pages/workspace/proposal_details.dart @@ -0,0 +1,161 @@ +import 'package:catalyst_voices/pages/workspace/proposal_segment_controller.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_proposal_navigation_ext.dart'; +import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +class ProposalDetails extends StatelessWidget { + final WorkspaceProposalNavigation navigation; + + const ProposalDetails({ + super.key, + required this.navigation, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.only(top: 10), + itemCount: navigation.segments.length, + itemBuilder: (context, index) { + final segment = navigation.segments[index]; + + return _ListenableSegmentDetails( + key: ValueKey('ListenableSegment${segment.id}DetailsKey'), + segment: segment, + controller: ProposalControllerScope.of( + context, + id: segment.id, + ), + ); + }, + ); + } +} + +class _ListenableSegmentDetails extends StatelessWidget { + final WorkspaceProposalSegment segment; + final VoicesNodeMenuController controller; + + const _ListenableSegmentDetails({ + super.key, + required this.segment, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, _) { + return _SegmentDetails( + key: ValueKey('Segment${segment.id}DetailsKey'), + name: segment.localizedName(context.l10n), + steps: segment.steps, + selected: controller.selected, + isExpanded: controller.isExpanded, + onChevronTap: () { + controller.isExpanded = !controller.isExpanded; + }, + ); + }, + ); + } +} + +class _SegmentDetails extends StatelessWidget { + final String name; + final List steps; + final int? selected; + final bool isExpanded; + final VoidCallback? onChevronTap; + + const _SegmentDetails({ + super.key, + required this.name, + required this.steps, + this.selected, + this.isExpanded = false, + this.onChevronTap, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SegmentHeader( + leading: ChevronExpandButton( + onTap: onChevronTap, + isExpanded: isExpanded, + ), + name: name, + isSelected: isExpanded, + ), + if (isExpanded) + ...steps.map( + (step) { + return _StepDetails( + key: ValueKey('WorkspaceStep${step.id}TileKey'), + id: step.id, + name: step.title, + desc: step.description, + doc: step.document, + isSelected: step.id == selected, + isEditable: step.isEditable, + ); + }, + ), + SizedBox(height: 24), + ].separatedBy(SizedBox(height: 12)).toList(), + ); + } +} + +class _StepDetails extends StatelessWidget { + const _StepDetails({ + super.key, + required this.id, + required this.name, + this.desc, + this.doc, + this.isSelected = false, + this.isEditable = false, + }); + + final int id; + final String name; + final String? desc; + final Document? doc; + final bool isSelected; + final bool isEditable; + + @override + Widget build(BuildContext context) { + return (desc != null) + ? WorkspaceTextTileContainer( + name: name, + isSelected: isSelected, + headerActions: [ + TextButton( + child: Text( + context.l10n.stepEdit, + style: Theme.of(context).textTheme.labelSmall, + ), + onPressed: isEditable ? () {} : null, + ), + ], + content: desc!, + ) + : WorkspaceTileContainer( + isSelected: isSelected, + content: VoicesRichText( + title: name, + document: doc, + ), + ); + } +} diff --git a/catalyst_voices/lib/pages/workspace/proposal_navigation_panel.dart b/catalyst_voices/lib/pages/workspace/proposal_navigation_panel.dart new file mode 100644 index 00000000000..3405aa98dc1 --- /dev/null +++ b/catalyst_voices/lib/pages/workspace/proposal_navigation_panel.dart @@ -0,0 +1,72 @@ +import 'package:catalyst_voices/pages/workspace/proposal_segment_controller.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_proposal_navigation_ext.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class ProposalNavigationPanel extends StatelessWidget { + final WorkspaceProposalNavigation navigation; + + const ProposalNavigationPanel({ + required this.navigation, + }); + + @override + Widget build(BuildContext context) { + return SpaceSidePanel( + isLeft: true, + name: context.l10n.workspaceProposalNavigation, + onCollapseTap: () {}, + tabs: [ + if (navigation.segments.isNotEmpty) + SpaceSidePanelTab( + name: context.l10n.workspaceProposalNavigationSegments, + body: Column( + children: navigation.segments.map( + (segment) { + return _ProposalSegmentBody( + key: ValueKey('ProposalSegment${segment.id}Key'), + segment: segment, + controller: ProposalControllerScope.of( + context, + id: segment.id, + ), + ); + }, + ).toList(), + ), + ), + ], + ); + } +} + +class _ProposalSegmentBody extends StatelessWidget { + final WorkspaceProposalSegment segment; + final VoicesNodeMenuController? controller; + + const _ProposalSegmentBody({ + super.key, + required this.segment, + this.controller, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return VoicesNodeMenu( + name: segment.localizedName(l10n), + controller: controller, + items: segment.steps.map( + (step) { + return VoicesNodeMenuItem( + id: step.id, + label: step.title, + ); + }, + ).toList(), + ); + } +} diff --git a/catalyst_voices/lib/pages/workspace/proposal_segment_controller.dart b/catalyst_voices/lib/pages/workspace/proposal_segment_controller.dart new file mode 100644 index 00000000000..c7a3fb2e4d0 --- /dev/null +++ b/catalyst_voices/lib/pages/workspace/proposal_segment_controller.dart @@ -0,0 +1,115 @@ +import 'package:catalyst_voices/widgets/menu/voices_node_menu.dart'; +import 'package:flutter/material.dart'; + +typedef ProposalControllerBuilder = ProposalController Function(Object id); + +final class ProposalControllerStateData extends VoicesNodeMenuStateData { + const ProposalControllerStateData({ + super.selectedItemId, + super.isExpanded, + }); +} + +/// Direct extension of [VoicesNodeMenuController]. +/// Probably we'll need extend controller with additional fields. +final class ProposalController extends VoicesNodeMenuController { + ProposalController(ProposalControllerStateData super._value); +} + +/// Keeps together [ProposalControllerStateData] tied to ids. +class ProposalControllerScope extends StatefulWidget { + final ProposalControllerBuilder builder; + final Widget child; + + const ProposalControllerScope({ + super.key, + required this.builder, + required this.child, + }); + + /// The closes instance of [ProposalControllerScope] + /// that encloses the given context, or null if none found. + /// + /// Uses [builder] with given [id] to build [ProposalController] + /// if none already created for this [id]. + static ProposalController? maybeOf( + BuildContext context, { + required Object id, + }) { + return context + .findAncestorStateOfType<_ProposalControllerScopeState>() + ?._getSegmentController(id); + } + + /// Wrapper on [maybeOf] but forcing null unwrapping. + static ProposalController of( + BuildContext context, { + required Object id, + }) { + final controller = maybeOf(context, id: id); + + assert( + controller != null, + 'Unable to find ProposalControllerScope as parent widget', + ); + + return controller!; + } + + @override + State createState() { + return _ProposalControllerScopeState(); + } +} + +class _ProposalControllerScopeState extends State { + final _cache = {}; + + bool _debugDisposed = false; + + static bool _debugAssertNotDisposed( + _ProposalControllerScopeState screenState, + ) { + assert(() { + if (screenState._debugDisposed) { + throw FlutterError( + 'A ${screenState.runtimeType} was used after being disposed.\n' + 'Once you have called dispose() on a ${screenState.runtimeType}, it ' + 'can no longer be used.', + ); + } + return true; + }()); + return true; + } + + @override + void dispose() { + assert(_debugAssertNotDisposed(this)); + assert(() { + _debugDisposed = true; + return true; + }()); + + final controllers = List.of(_cache.values); + for (final controller in controllers) { + controller.dispose(); + } + _cache.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + ProposalController _getSegmentController(Object segmentId) { + _debugAssertNotDisposed(this); + + return _cache.putIfAbsent( + segmentId, + () => widget.builder(segmentId), + ); + } +} diff --git a/catalyst_voices/lib/pages/workspace/proposal_setup_panel.dart b/catalyst_voices/lib/pages/workspace/proposal_setup_panel.dart new file mode 100644 index 00000000000..532890a0c7c --- /dev/null +++ b/catalyst_voices/lib/pages/workspace/proposal_setup_panel.dart @@ -0,0 +1,30 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; + +class ProposalSetupPanel extends StatelessWidget { + const ProposalSetupPanel(); + + @override + Widget build(BuildContext context) { + return SpaceSidePanel( + isLeft: false, + name: context.l10n.workspaceProposalSetup, + onCollapseTap: () {}, + tabs: [ + SpaceSidePanelTab( + name: 'Guidance', + body: Offstage(), + ), + SpaceSidePanelTab( + name: 'Comments', + body: Offstage(), + ), + SpaceSidePanelTab( + name: 'Actions', + body: Offstage(), + ), + ], + ); + } +} diff --git a/catalyst_voices/lib/pages/workspace/sample_rich_text.dart b/catalyst_voices/lib/pages/workspace/sample_rich_text.dart new file mode 100644 index 00000000000..69534b18b3b --- /dev/null +++ b/catalyst_voices/lib/pages/workspace/sample_rich_text.dart @@ -0,0 +1,74 @@ +const sampleRichText = [ + { + "insert": { + "image": + 'https://upload.wikimedia.org/wikipedia/commons/b/b6/Image_created_with_a_mobile_phone.png' + }, + "attributes": {"style": "width: 181.764; height: 140; "} + }, + {"insert": "\n\n"}, + { + "insert": "Legend Tells About Amazonian The Great Smith", + "attributes": {"bold": true} + }, + {"insert": "\n\nAn ancient legend confirms "}, + { + "insert": "Amazonian as the Father of the Samurai Sword. Amazonian", + "attributes": {"bold": true} + }, + {"insert": " and his son, "}, + { + "insert": "Amateur", + "attributes": {"italic": true} + }, + { + "insert": + ", were the prominent smiths who led a team of armorers, employed by Emperor Mommy (683-707) to make swords for his army of warriors. Later his son, Amateur continued his father's " + }, + { + "insert": "great work", + "attributes": {"italic": true} + }, + {"insert": ".\n\n"}, + { + "insert": "Amateur", + "attributes": {"italic": true} + }, + { + "insert": "\n", + "attributes": {"list": "bullet"} + }, + { + "insert": "Amauroses", + "attributes": {"italic": true} + }, + { + "insert": "\n", + "attributes": {"list": "bullet"} + }, + { + "insert": "Amateurism", + "attributes": {"italic": true} + }, + { + "insert": "\n", + "attributes": {"list": "bullet"} + }, + {"insert": "\n"}, + { + "insert": "Sword 1", + "attributes": {"italic": true} + }, + { + "insert": "\n", + "attributes": {"list": "ordered"} + }, + { + "insert": "Sword 2", + "attributes": {"italic": true} + }, + { + "insert": "\n", + "attributes": {"list": "ordered"} + } +]; diff --git a/catalyst_voices/lib/pages/workspace/workspace_page.dart b/catalyst_voices/lib/pages/workspace/workspace_page.dart index ee8312603a2..ab314c65988 100644 --- a/catalyst_voices/lib/pages/workspace/workspace_page.dart +++ b/catalyst_voices/lib/pages/workspace/workspace_page.dart @@ -1,17 +1,85 @@ +import 'package:catalyst_voices/pages/workspace/proposal_details.dart'; +import 'package:catalyst_voices/pages/workspace/proposal_navigation_panel.dart'; +import 'package:catalyst_voices/pages/workspace/proposal_segment_controller.dart'; +import 'package:catalyst_voices/pages/workspace/proposal_setup_panel.dart'; +import 'package:catalyst_voices/pages/workspace/sample_rich_text.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; -class WorkspacePage extends StatelessWidget { - const WorkspacePage({super.key}); +const _setupSegmentId = 'setup'; +final _proposalNavigation = WorkspaceProposalNavigation( + segments: [ + WorkspaceProposalSetup( + id: _setupSegmentId, + steps: [ + WorkspaceProposalSegmentStep( + id: 0, + title: 'Title', + description: 'F14 / Promote Social Entrepreneurs and a ' + 'longer title up-to 60 characters', + isEditable: true, + ), + WorkspaceProposalSegmentStep( + id: 1, + title: 'Rich text', + document: Document.fromJson(sampleRichText), + isEditable: true, + ), + WorkspaceProposalSegmentStep( + id: 2, + title: 'Other topic', + description: 'Other topic', + isEditable: false, + ), + WorkspaceProposalSegmentStep( + id: 3, + title: 'Other topic', + description: 'Other topic', + isEditable: false, + ), + ], + ), + ], +); + +class WorkspacePage extends StatefulWidget { + const WorkspacePage({ + super.key, + }); + + @override + State createState() => _WorkspacePageState(); +} + +class _WorkspacePageState extends State { @override Widget build(BuildContext context) { - return Container( - color: Colors.teal, - alignment: Alignment.center, - child: Text( - 'Workspace', - style: Theme.of(context).textTheme.titleLarge, + return ProposalControllerScope( + builder: _buildSegmentController, + child: SpaceScaffold( + left: ProposalNavigationPanel( + navigation: _proposalNavigation, + ), + right: ProposalSetupPanel(), + child: ProposalDetails( + navigation: _proposalNavigation, + ), ), ); } + + // Only creates initial controller one time + ProposalController _buildSegmentController(Object segmentId) { + final value = segmentId == _setupSegmentId + ? ProposalControllerStateData( + selectedItemId: 0, + isExpanded: true, + ) + : ProposalControllerStateData(); + + return ProposalController(value); + } } diff --git a/catalyst_voices/lib/pages/workspace/workspace_proposal_navigation_ext.dart b/catalyst_voices/lib/pages/workspace/workspace_proposal_navigation_ext.dart new file mode 100644 index 00000000000..0642f3cf230 --- /dev/null +++ b/catalyst_voices/lib/pages/workspace/workspace_proposal_navigation_ext.dart @@ -0,0 +1,10 @@ +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +extension WorkspaceProposalSegmentExt on WorkspaceProposalSegment { + String localizedName(VoicesLocalizations localizations) { + return switch (this) { + WorkspaceProposalSetup() => localizations.workspaceProposalSetup, + }; + } +} diff --git a/catalyst_voices/lib/widgets/common/proposal_status_container.dart b/catalyst_voices/lib/widgets/common/proposal_status_container.dart index 919d540b5a8..adb1f59df51 100644 --- a/catalyst_voices/lib/widgets/common/proposal_status_container.dart +++ b/catalyst_voices/lib/widgets/common/proposal_status_container.dart @@ -18,7 +18,7 @@ class ProposalStatusContainer extends StatelessWidget { final config = type._config(context); final iconTheme = IconThemeData( - size: 18, + size: 16, color: config.iconColor, ); @@ -85,6 +85,13 @@ extension _ProposalStatusExt on ProposalStatus { textColor: colors.textPrimary, backgroundColor: colors.onSurfaceNeutralOpaqueLv1, ), + ProposalStatus.inProgress => _ProposalStatusContainerConfig( + iconData: CatalystVoicesIcons.annotation, + iconColor: colors.iconsPrimary, + text: context.l10n.proposalStatusInProgress, + textColor: colors.textPrimary, + backgroundColor: colors.onSurfaceNeutralOpaqueLv1, + ), }; } } diff --git a/catalyst_voices/lib/widgets/common/proposal_status_indicator.dart b/catalyst_voices/lib/widgets/common/proposal_status_indicator.dart deleted file mode 100644 index baef0dad6fc..00000000000 --- a/catalyst_voices/lib/widgets/common/proposal_status_indicator.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:flutter/material.dart'; - -class ProposalStatusIndicator extends StatelessWidget { - final ProposalStatus type; - - const ProposalStatusIndicator({ - super.key, - required this.type, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final config = type._config(context); - - final iconTheme = IconThemeData( - size: 18, - color: config.iconColor, - ); - - final textStyle = (theme.textTheme.labelLarge ?? TextStyle()).copyWith( - color: config.textColor, - ); - - return IconTheme( - data: iconTheme, - child: DefaultTextStyle( - style: textStyle, - child: Container( - padding: EdgeInsets.all(8).add(EdgeInsets.only(right: 4)), - decoration: BoxDecoration( - color: config.backgroundColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(config.iconData), - SizedBox(width: 8), - Text(config.text) - ], - ), - ), - ), - ); - } -} - -final class _ProposalStatusIndicatorConfig { - final IconData iconData; - final Color? iconColor; - final String text; - final Color? textColor; - final Color? backgroundColor; - - _ProposalStatusIndicatorConfig({ - required this.iconData, - this.iconColor, - required this.text, - this.textColor, - this.backgroundColor, - }); -} - -extension _ProposalStatusExt on ProposalStatus { - _ProposalStatusIndicatorConfig _config(BuildContext context) { - final colors = Theme.of(context).colors; - - return switch (this) { - ProposalStatus.ready => _ProposalStatusIndicatorConfig( - iconData: CatalystVoicesIcons.check, - iconColor: colors.iconsSuccess, - text: context.l10n.proposalStatusReady, - textColor: colors.textPrimary, - backgroundColor: colors.successContainer, - ), - ProposalStatus.draft => _ProposalStatusIndicatorConfig( - iconData: CatalystVoicesIcons.pencil_alt, - iconColor: colors.iconsForeground, - text: context.l10n.proposalStatusDraft, - textColor: colors.textPrimary, - backgroundColor: colors.onSurfaceNeutralOpaqueLv1, - ), - }; - } -} diff --git a/catalyst_voices/lib/widgets/containers/workspace_text_tile_container.dart b/catalyst_voices/lib/widgets/containers/workspace_text_tile_container.dart new file mode 100644 index 00000000000..0a58f4064b8 --- /dev/null +++ b/catalyst_voices/lib/widgets/containers/workspace_text_tile_container.dart @@ -0,0 +1,122 @@ +import 'package:catalyst_voices/widgets/headers/segment_header.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +/// Opinionated container usual used inside space main body. +class WorkspaceTextTileContainer extends StatelessWidget { + final bool isSelected; + final String name; + final List headerActions; + final String content; + final Widget? footer; + + const WorkspaceTextTileContainer({ + super.key, + this.isSelected = false, + required this.name, + this.headerActions = const [], + required this.content, + this.footer, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AnimatedContainer( + duration: kThemeChangeDuration, + decoration: BoxDecoration( + color: theme.colors.elevationsOnSurfaceNeutralLv1White, + borderRadius: BorderRadius.horizontal( + left: isSelected ? Radius.zero : Radius.circular(28), + right: Radius.circular(28), + ), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0!, + offset: Offset(0, 1), + blurRadius: 4, + ), + ], + ), + foregroundDecoration: BoxDecoration( + border: Border( + left: isSelected + ? BorderSide( + color: theme.colorScheme.primary, + width: 5, + ) + : BorderSide.none, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Header(name, headerActions), + _Content(child: content), + _Footer(child: footer), + ], + ), + ); + } +} + +class _Header extends StatelessWidget { + final String name; + final List actions; + + _Header(this.name, this.actions); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: SegmentHeader( + name: name, + actions: actions, + ), + ); + } +} + +class _Content extends StatelessWidget { + final String child; + + const _Content({ + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final style = (theme.textTheme.bodyMedium ?? TextStyle()).copyWith( + color: theme.colors.textOnPrimary, + ); + + return DefaultTextStyle( + style: style, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Text(child), + ), + ); + } +} + +class _Footer extends StatelessWidget { + final Widget? child; + + const _Footer({ + this.child, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints(minHeight: 16), + child: child, + ); + } +} diff --git a/catalyst_voices/lib/widgets/containers/workspace_tile_container.dart b/catalyst_voices/lib/widgets/containers/workspace_tile_container.dart index 65290602e5e..9d54f7f0fdb 100644 --- a/catalyst_voices/lib/widgets/containers/workspace_tile_container.dart +++ b/catalyst_voices/lib/widgets/containers/workspace_tile_container.dart @@ -1,22 +1,15 @@ -import 'package:catalyst_voices/widgets/headers/segment_header.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; /// Opinionated container usual used inside space main body. class WorkspaceTileContainer extends StatelessWidget { final bool isSelected; - final String name; - final List headerActions; final Widget content; - final Widget? footer; const WorkspaceTileContainer({ super.key, this.isSelected = false, - required this.name, - this.headerActions = const [], required this.content, - this.footer, }); @override @@ -27,10 +20,14 @@ class WorkspaceTileContainer extends StatelessWidget { duration: kThemeChangeDuration, decoration: BoxDecoration( color: theme.colors.elevationsOnSurfaceNeutralLv1White, - borderRadius: BorderRadius.horizontal( - left: isSelected ? Radius.zero : Radius.circular(28), - right: Radius.circular(28), - ), + borderRadius: _borderRadius(isSelected), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0!, + offset: Offset(0, 1), + blurRadius: 4, + ), + ], ), foregroundDecoration: BoxDecoration( border: Border( @@ -42,74 +39,15 @@ class WorkspaceTileContainer extends StatelessWidget { : BorderSide.none, ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Header(name, headerActions), - _Content(child: content), - _Footer(child: footer), - ], + child: ClipRRect( + child: content, + borderRadius: _borderRadius(isSelected), ), ); } } -class _Header extends StatelessWidget { - final String name; - final List actions; - - _Header(this.name, this.actions); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: SegmentHeader( - name: name, - actions: actions, - ), +BorderRadius _borderRadius(bool isSelected) => BorderRadius.horizontal( + left: isSelected ? Radius.zero : Radius.circular(28), + right: Radius.circular(28), ); - } -} - -class _Content extends StatelessWidget { - final Widget child; - - const _Content({ - required this.child, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final style = (theme.textTheme.bodyMedium ?? TextStyle()).copyWith( - color: theme.colors.textOnPrimary, - ); - - return DefaultTextStyle( - style: style, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: child, - ), - ); - } -} - -class _Footer extends StatelessWidget { - final Widget? child; - - const _Footer({ - this.child, - }); - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints(minHeight: 16), - child: child, - ); - } -} diff --git a/catalyst_voices/lib/widgets/drawer/voices_drawer.dart b/catalyst_voices/lib/widgets/drawer/voices_drawer.dart index 8f336a4b5b5..a733ff0e02b 100644 --- a/catalyst_voices/lib/widgets/drawer/voices_drawer.dart +++ b/catalyst_voices/lib/widgets/drawer/voices_drawer.dart @@ -41,6 +41,7 @@ class VoicesDrawer extends StatelessWidget { ), ), child: Drawer( + width: 350, shape: const RoundedRectangleBorder(), child: Column( children: [ diff --git a/catalyst_voices/lib/widgets/drawer/voices_drawer_nav_item.dart b/catalyst_voices/lib/widgets/drawer/voices_drawer_nav_item.dart index 3dc17d2c6c0..ff56551e90e 100644 --- a/catalyst_voices/lib/widgets/drawer/voices_drawer_nav_item.dart +++ b/catalyst_voices/lib/widgets/drawer/voices_drawer_nav_item.dart @@ -3,7 +3,6 @@ import 'package:catalyst_voices/widgets/common/proposal_status_container.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/material.dart'; class VoicesDrawerNavItem extends StatelessWidget { @@ -24,7 +23,7 @@ class VoicesDrawerNavItem extends StatelessWidget { fixedSize: WidgetStatePropertyAll(Size.square(48)), ); - final nameTextStyle = theme.textTheme.labelLarge?.copyWith( + final nameTextStyle = theme.textTheme.labelMedium?.copyWith( color: theme.colors.textPrimary, ); @@ -45,11 +44,12 @@ class VoicesDrawerNavItem extends StatelessWidget { ), ), ProposalStatusContainer(type: status), + SizedBox(width: 8), VoicesIconButton( child: Icon(CatalystVoicesIcons.dots_vertical), onTap: () {}, ), - ].separatedBy(SizedBox(width: 12)).toList(), + ], ), ), ); diff --git a/catalyst_voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/lib/widgets/rich_text/voices_rich_text.dart index 11431a537d9..144467ea646 100644 --- a/catalyst_voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/lib/widgets/rich_text/voices_rich_text.dart @@ -44,7 +44,7 @@ class _Editor extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: ResizableBoxParent( minHeight: 400, resizableVertically: true, @@ -53,21 +53,24 @@ class _Editor extends StatelessWidget { decoration: BoxDecoration( color: editMode ? Theme.of(context).colors.onSurfaceNeutralOpaqueLv1 - : Theme.of(context).colors.onSurfaceNeutralOpaqueLv0, + : Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, border: Border.all( color: Theme.of(context).colorScheme.outlineVariant, ), borderRadius: BorderRadius.circular(8), ), - child: QuillEditor.basic( - controller: controller, - focusNode: focusNode, - configurations: QuillEditorConfigurations( - padding: const EdgeInsets.all(16), - placeholder: context.l10n.placeholderRichText, - embedBuilders: CatalystPlatform.isWeb - ? FlutterQuillEmbeds.editorWebBuilders() - : FlutterQuillEmbeds.editorBuilders(), + child: IgnorePointer( + ignoring: !editMode, + child: QuillEditor.basic( + controller: controller, + focusNode: focusNode, + configurations: QuillEditorConfigurations( + padding: const EdgeInsets.all(16), + placeholder: context.l10n.placeholderRichText, + embedBuilders: CatalystPlatform.isWeb + ? FlutterQuillEmbeds.editorWebBuilders() + : FlutterQuillEmbeds.editorBuilders(), + ), ), ), ), @@ -245,7 +248,7 @@ class _TopBar extends StatelessWidget { style: Theme.of(context).textTheme.labelSmall, ), ), - SizedBox(width: 8), + SizedBox(width: 24), ], ); } @@ -255,52 +258,64 @@ class _VoicesRichTextState extends State { final QuillController _controller = QuillController.basic(); int _documentLength = 0; bool _editMode = false; + Document _preEditDocument = Document(); final FocusNode _focusNode = FocusNode(); @override Widget build(BuildContext context) { - return ExcludeFocus( - excluding: !_editMode, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 16, - top: 20, - bottom: 20, - ), - child: _TopBar( - title: widget.title, - editMode: _editMode, - onToggleEditMode: () { - setState(() { - _editMode = !_editMode; - }); - }, - ), + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 24, + top: 20, + bottom: 20, ), - if (_editMode) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: _Toolbar(controller: _controller), - ), - _Editor( + child: _TopBar( + title: widget.title, editMode: _editMode, - controller: _controller, - focusNode: _focusNode, + onToggleEditMode: () { + setState(() { + if (_editMode) { + _controller.document = + Document.fromDelta(_preEditDocument.toDelta()); + } else { + _preEditDocument = + Document.fromDelta(_controller.document.toDelta()); + } + _editMode = !_editMode; + }); + }, ), - if (widget.charsLimit != null) - _Limit( - documentLength: _documentLength, - charsLimit: widget.charsLimit!, - ), - SizedBox(height: 16), - _Footer( - controller: _controller, - onSave: widget.onSave, + ), + if (_editMode) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: _Toolbar(controller: _controller), ), - ], - ), + _Editor( + editMode: _editMode, + controller: _controller, + focusNode: _focusNode, + ), + if (widget.charsLimit != null) + _Limit( + documentLength: _documentLength, + charsLimit: widget.charsLimit!, + ), + SizedBox(height: 16), + (_editMode) + ? _Footer( + controller: _controller, + onSave: (document) { + widget.onSave?.call(document); + setState(() { + _editMode = false; + }); + }, + ) + : SizedBox(height: 24), + ], ); } diff --git a/catalyst_voices/lib/widgets/widgets.dart b/catalyst_voices/lib/widgets/widgets.dart index 3c4d945fb16..d5c277f9419 100644 --- a/catalyst_voices/lib/widgets/widgets.dart +++ b/catalyst_voices/lib/widgets/widgets.dart @@ -17,6 +17,7 @@ export 'containers/sidebar_scaffold.dart'; export 'containers/space_scaffold.dart'; export 'containers/space_side_panel.dart'; export 'containers/workspace_tile_container.dart'; +export 'containers/workspace_text_tile_container.dart'; export 'drawer/voices_drawer.dart'; export 'drawer/voices_drawer_nav_item.dart'; export 'drawer/voices_drawer_space_chooser.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart b/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart index 8932c457c84..39255c360df 100644 --- a/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart +++ b/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart @@ -227,7 +227,7 @@ class VoicesColors { static const Color lightAvatarsWarning = Color(0xFFFDE1CE); /// Color: #FFFFFF - static const Color lightElevationsOnSurfaceNeutralLv0 = Color(0xFFFFFFFF); + static const Color lightElevationsOnSurfaceNeutralLv0 = Color(0x16123CD3); /// Color: #F2F4F8 static const Color lightElevationsOnSurfaceNeutralLv1Grey = Color(0xFFF2F4F8); diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index 62484792398..ccd32c13d8b 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -334,6 +334,12 @@ abstract class VoicesLocalizations { /// **'Draft'** String get proposalStatusDraft; + /// Indicates to user that status is in progress + /// + /// In en, this message translates to: + /// **'In progress'** + String get proposalStatusInProgress; + /// Label shown on a proposal card indicating that the proposal is funded. /// /// In en, this message translates to: @@ -442,11 +448,29 @@ abstract class VoicesLocalizations { /// **'Campaign title'** String get treasuryCampaignTitle; - /// Button name in treasury step + /// Button name in step /// /// In en, this message translates to: /// **'Edit'** - String get treasuryStepEdit; + String get stepEdit; + + /// Left panel name in workspace + /// + /// In en, this message translates to: + /// **'Proposal navigation'** + String get workspaceProposalNavigation; + + /// Tab name in proposal setup panel + /// + /// In en, this message translates to: + /// **'Segments'** + String get workspaceProposalNavigationSegments; + + /// Segment name + /// + /// In en, this message translates to: + /// **'Proposal setup'** + String get workspaceProposalSetup; /// Name shown in spaces shell drawer /// diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index 848aa952b2b..b26ad9e0a12 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -130,6 +130,9 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get proposalStatusDraft => 'Draft'; + @override + String get proposalStatusInProgress => 'In progress'; + @override String get fundedProposal => 'Funded proposal'; @@ -234,7 +237,16 @@ class VoicesLocalizationsEn extends VoicesLocalizations { String get treasuryCampaignTitle => 'Campaign title'; @override - String get treasuryStepEdit => 'Edit'; + String get stepEdit => 'Edit'; + + @override + String get workspaceProposalNavigation => 'Proposal navigation'; + + @override + String get workspaceProposalNavigationSegments => 'Segments'; + + @override + String get workspaceProposalSetup => 'Proposal setup'; @override String get drawerSpaceTreasury => 'Treasury'; diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index 7049f13b836..3bcc135788c 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -130,6 +130,9 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get proposalStatusDraft => 'Draft'; + @override + String get proposalStatusInProgress => 'In progress'; + @override String get fundedProposal => 'Funded proposal'; @@ -234,7 +237,16 @@ class VoicesLocalizationsEs extends VoicesLocalizations { String get treasuryCampaignTitle => 'Campaign title'; @override - String get treasuryStepEdit => 'Edit'; + String get stepEdit => 'Edit'; + + @override + String get workspaceProposalNavigation => 'Proposal navigation'; + + @override + String get workspaceProposalNavigationSegments => 'Segments'; + + @override + String get workspaceProposalSetup => 'Proposal setup'; @override String get drawerSpaceTreasury => 'Treasury'; diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb index a4fe26a9ade..66fe4ab2677 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -165,6 +165,10 @@ "@proposalStatusDraft": { "description": "Indicates to user that status is in draft mode" }, + "proposalStatusInProgress": "In progress", + "@proposalStatusInProgress": { + "description": "Indicates to user that status is in progress" + }, "fundedProposal": "Funded proposal", "@fundedProposal": { "description": "Label shown on a proposal card indicating that the proposal is funded." @@ -273,9 +277,21 @@ "@treasuryCampaignTitle": { "description": "Campaign title" }, - "treasuryStepEdit": "Edit", - "@treasuryStepEdit": { - "description": "Button name in treasury step" + "stepEdit": "Edit", + "@stepEdit": { + "description": "Button name in step" + }, + "workspaceProposalNavigation": "Proposal navigation", + "@workspaceProposalNavigation": { + "description": "Left panel name in workspace" + }, + "workspaceProposalNavigationSegments": "Segments", + "@workspaceProposalNavigationSegments": { + "description": "Tab name in proposal setup panel" + }, + "workspaceProposalSetup": "Proposal setup", + "@workspaceProposalSetup": { + "description": "Segment name" }, "drawerSpaceTreasury": "Treasury", "@drawerSpaceTreasury": { diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart index 3cd21da9416..c572ebd5280 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -10,4 +10,7 @@ export 'space.dart'; export 'treasury/treasury_campaign_builder.dart'; export 'treasury/treasury_campaign_segment.dart'; export 'treasury/treasury_campaign_segment_step.dart'; +export 'workspace/workspace_proposal_navigation.dart'; +export 'workspace/workspace_proposal_segment.dart'; +export 'workspace/workspace_proposal_segment_step.dart'; export 'user/user.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/proposal_status.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/proposal_status.dart index a53236b7676..cb21b0e7f4c 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/proposal_status.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/proposal_status.dart @@ -1,4 +1,5 @@ enum ProposalStatus { ready, - draft; + draft, + inProgress; } diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_navigation.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_navigation.dart new file mode 100644 index 00000000000..56d34287d1f --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_navigation.dart @@ -0,0 +1,15 @@ +import 'package:catalyst_voices_models/src/workspace/workspace_proposal_segment.dart'; +import 'package:equatable/equatable.dart'; + +final class WorkspaceProposalNavigation extends Equatable { + final List segments; + + WorkspaceProposalNavigation({ + required this.segments, + }); + + @override + List get props => [ + segments, + ]; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_segment.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_segment.dart new file mode 100644 index 00000000000..797c54f1df7 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_segment.dart @@ -0,0 +1,25 @@ +import 'package:catalyst_voices_models/src/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +sealed class WorkspaceProposalSegment extends Equatable { + final Object id; + final List steps; + + const WorkspaceProposalSegment({ + required this.id, + required this.steps, + }); + + @override + List get props => [ + id, + steps, + ]; +} + +final class WorkspaceProposalSetup extends WorkspaceProposalSegment { + const WorkspaceProposalSetup({ + required super.id, + required super.steps, + }); +} diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_segment_step.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_segment_step.dart new file mode 100644 index 00000000000..8feb25d5124 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/workspace/workspace_proposal_segment_step.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +class WorkspaceProposalSegmentStep extends Equatable { + final int id; + final String title; + final String? description; + final Document? document; + final bool isEditable; + + WorkspaceProposalSegmentStep({ + required this.id, + required this.title, + this.description, + this.document, + this.isEditable = false, + }) : assert( + description != null || document != null, + 'Make sure description or document are provided', + ); + + @override + List get props => [ + id, + title, + description, + document, + isEditable, + ]; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml index bf137446228..df456f50f1b 100644 --- a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: catalyst_cardano_serialization: ^0.4.0 equatable: ^2.0.5 + flutter_quill: ^10.5.13 meta: ^1.10.0 dev_dependencies: diff --git a/catalyst_voices/pubspec.yaml b/catalyst_voices/pubspec.yaml index 0bc55a9ac6e..f2717fdf610 100644 --- a/catalyst_voices/pubspec.yaml +++ b/catalyst_voices/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: flutter_adaptive_scaffold: ^0.2.4 flutter_bloc: ^8.1.5 flutter_localized_locales: ^2.0.5 - flutter_quill: ^10.5.0 + flutter_quill: ^10.5.13 flutter_quill_extensions: ^10.5.13 flutter_web_plugins: sdk: flutter diff --git a/catalyst_voices/uikit_example/pubspec.yaml b/catalyst_voices/uikit_example/pubspec.yaml index 3afda02f337..5d030984188 100644 --- a/catalyst_voices/uikit_example/pubspec.yaml +++ b/catalyst_voices/uikit_example/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: sdk: flutter flutter_bloc: ^8.1.5 flutter_localized_locales: ^2.0.5 - flutter_quill: ^10.5.0 + flutter_quill: ^10.5.13 dev_dependencies: build_runner: ^2.4.12 diff --git a/melos.yaml b/melos.yaml index 1e465aae7d1..9c7beb7ce28 100644 --- a/melos.yaml +++ b/melos.yaml @@ -24,7 +24,7 @@ command: equatable: ^2.0.5 flutter_bloc: ^8.1.5 flutter_localized_locales: ^2.0.5 - flutter_quill: ^10.5.0 + flutter_quill: ^10.5.13 flutter_quill_extensions: ^10.5.13 formz: ^0.7.0 logging: ^1.2.0