Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.77.0

## Feature

- Added `ImpaktfullUiKanbanBoard` component with configurable columns, drag-and-drop support, and customizable cards with images

# 0.76.3

## Chore
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Update CHANGELOG.md to match what changed in the code:

Add a new version entry at the top of CHANGELOG.md with the appropriate section (Feature, Fix, Chore, etc.).

Do not update the `pubspec.yaml` version. This is handled by the CI/CD pipeline.

## Validate

```bash
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ Components are always prefixed with `ImpaktfullUi` to avoid conflicts with other
- ImpaktfullUiImageCrop
- ImpaktfullUiInputField
- ImpaktfullUiIntroduction
- ImpaktfullUiKanbanBoard
- ImpaktfullUiKanbanBoardColumn
- ImpaktfullUiKanbanBoardCard
- ImpaktfullUiLineChart
- ImpaktfullUiListItem
- ImpaktfullUiSimpleListItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import 'package:impaktfull_ui_example/src/component_library/items/icon_button/ic
import 'package:impaktfull_ui_example/src/component_library/items/image_crop/image_crop_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/input_field/input_field_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/introduction/introduction_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/kanban_board/kanban_board_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/line_chart/line_chart_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/list_item/list_item_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/list_view/list_view_library_item.dart';
Expand Down Expand Up @@ -136,6 +137,7 @@ class ComponentLibrary {
const ImageCropLibraryItem(),
const InputFieldLibraryItem(),
const IntroductionLibraryItem(),
const KanbanBoardLibraryItem(),
const LineChartLibraryItem(),
const ListItemLibraryItem(),
const ListViewLibraryItem(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:impaktfull_ui_example/src/component_library/config/component_library_inputs.dart';
import 'package:impaktfull_ui_example/src/component_library/config/component_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/kanban_board/kanban_board_library_variant.dart';

class KanbanBoardLibraryItem extends ComponentLibraryItem {
const KanbanBoardLibraryItem();

@override
String get title => 'ImpaktfullUiKanbanBoard';

@override
List<ComponentLibraryVariant> getComponentVariants() => [
const KanbanBoardLibraryVariant(),
];
}

class KanbanBoardLibraryInputs extends ComponentLibraryInputs {
@override
List<ComponentLibraryInputItem> buildInputItems() => [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:impaktfull_ui/impaktfull_ui.dart';
import 'package:impaktfull_ui_example/src/component_library/config/component_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/kanban_board/kanban_board_library_item.dart';
import 'package:impaktfull_ui_example/src/component_library/items/kanban_board/widget/kanban_board_state_configurator.dart';

class KanbanBoardLibraryVariant
extends ComponentLibraryVariant<KanbanBoardLibraryVariantInputs> {
const KanbanBoardLibraryVariant();

@override
String get title => 'Default';

@override
List<Widget> build(
BuildContext context, KanbanBoardLibraryVariantInputs inputs) {
return [
SizedBox(
height: 850,
child: KanbanBoardStateConfigurator(
builder: (context, items, onItemMoved, onItemReordered) =>
ImpaktfullUiKanbanBoard<String>(
columns: const [
ImpaktfullUiKanbanBoardColumnConfig(
id: 'todo',
name: 'To Do',
color: Colors.blue,
),
ImpaktfullUiKanbanBoardColumnConfig(
id: 'in_progress',
name: 'In Progress',
color: Colors.orange,
),
ImpaktfullUiKanbanBoardColumnConfig(
id: 'done',
name: 'Done',
color: Colors.green,
),
],
items: items,
itemBuilder: (item) => ImpaktfullUiKanbanBoardCard<String>(
key: ValueKey(item.id),
item: item,
),
onItemMoved: onItemMoved,
onItemReordered: onItemReordered,
),
),
),
];
}

@override
KanbanBoardLibraryVariantInputs inputs() => KanbanBoardLibraryVariantInputs();
}

class KanbanBoardLibraryVariantInputs extends KanbanBoardLibraryInputs {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:impaktfull_ui/impaktfull_ui.dart';

class KanbanBoardStateConfigurator extends StatefulWidget {
final Widget Function(
BuildContext context,
List<ImpaktfullUiKanbanBoardItem<String>> items,
void Function(
ImpaktfullUiKanbanBoardItem<String> item,
String fromColumnId,
String toColumnId,
int newIndex,
) onItemMoved,
void Function(
ImpaktfullUiKanbanBoardItem<String> item,
int oldIndex,
int newIndex,
) onItemReordered,
) builder;

const KanbanBoardStateConfigurator({
required this.builder,
super.key,
});

@override
State<KanbanBoardStateConfigurator> createState() =>
_KanbanBoardStateConfiguratorState();
}

class _KanbanBoardStateConfiguratorState
extends State<KanbanBoardStateConfigurator> {
late List<ImpaktfullUiKanbanBoardItem<String>> _items;

@override
void initState() {
super.initState();
_items = [
const ImpaktfullUiKanbanBoardItem(
id: '1',
columnId: 'todo',
title: 'Design new feature',
description: 'Create wireframes and mockups for the new dashboard',
),
const ImpaktfullUiKanbanBoardItem(
id: '2',
columnId: 'todo',
title: 'Write documentation',
description: 'Document the API endpoints',
imageUrl: 'https://picsum.photos',
),
const ImpaktfullUiKanbanBoardItem(
id: '3',
columnId: 'in_progress',
title: 'Implement login',
description: 'Add OAuth2 authentication flow',
),
const ImpaktfullUiKanbanBoardItem(
id: '4',
columnId: 'done',
title: 'Setup project',
description: 'Initialize repository and CI/CD',
),
];
}

void _onItemMoved(
ImpaktfullUiKanbanBoardItem<String> item,
String fromColumnId,
String toColumnId,
int newIndex,
) {
setState(() {
final itemIndex = _items.indexWhere((i) => i.id == item.id);
if (itemIndex != -1) {
_items[itemIndex] = ImpaktfullUiKanbanBoardItem(
id: item.id,
columnId: toColumnId,
title: item.title,
description: item.description,
imageUrl: item.imageUrl,
data: item.data,
);
}
});
debugPrint(
'Item ${item.title} moved from $fromColumnId to $toColumnId at index $newIndex');
}

void _onItemReordered(
ImpaktfullUiKanbanBoardItem<String> item,
int oldIndex,
int newIndex,
) {
setState(() {
final columnItems =
_items.where((i) => i.columnId == item.columnId).toList();
final movedItem = columnItems.removeAt(oldIndex);
columnItems.insert(
newIndex > oldIndex ? newIndex - 1 : newIndex, movedItem);

// Rebuild the full list maintaining column order
final otherItems =
_items.where((i) => i.columnId != item.columnId).toList();
_items = [...otherItems, ...columnItems];
});
debugPrint('Item ${item.title} reordered from $oldIndex to $newIndex');
}

@override
Widget build(BuildContext context) {
return widget.builder(
context,
_items,
_onItemMoved,
_onItemReordered,
);
}
}
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.76.3"
version: "0.77.1"
impaktfull_ui_figma_sync:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions lib/impaktfull_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export 'src/components/interaction_feedback/touch_feedback_sequence/touch_feedba
export 'src/components/introduction/introduction.dart';
export 'src/components/image_crop/image_crop.dart';
export 'src/components/input_field/input_field.dart';
export 'src/components/kanban_board/kanban_board.dart';
export 'src/components/line_chart/line_chart.dart';
export 'src/components/list_item/list_item.dart';
export 'src/components/list_view/list_view.dart';
Expand Down
114 changes: 114 additions & 0 deletions lib/src/components/kanban_board/kanban_board.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:impaktfull_ui/src/components/auto_layout/auto_layout.dart';
import 'package:impaktfull_ui/src/components/kanban_board/kanban_board_column.dart';
import 'package:impaktfull_ui/src/components/kanban_board/kanban_board_style.dart';
import 'package:impaktfull_ui/src/components/kanban_board/model/kanban_board_column_config.dart';
import 'package:impaktfull_ui/src/components/kanban_board/model/kanban_board_item.dart';
import 'package:impaktfull_ui/src/widget/override_components/overridable_component_builder.dart';

export 'kanban_board_card.dart';
export 'kanban_board_column.dart';
export 'kanban_board_style.dart';
export 'model/kanban_board_column_config.dart';
export 'model/kanban_board_item.dart';

class ImpaktfullUiKanbanBoard<T> extends StatefulWidget {
final List<ImpaktfullUiKanbanBoardColumnConfig> columns;
final List<ImpaktfullUiKanbanBoardItem<T>> items;
final Widget Function(ImpaktfullUiKanbanBoardItem<T> item)? itemBuilder;
final void Function(
ImpaktfullUiKanbanBoardItem<T> item,
String fromColumnId,
String toColumnId,
int newIndex,
)? onItemMoved;
final void Function(
ImpaktfullUiKanbanBoardItem<T> item,
int oldIndex,
int newIndex,
)? onItemReordered;
final ImpaktfullUiKanbanBoardTheme? theme;

const ImpaktfullUiKanbanBoard({
required this.columns,
required this.items,
this.itemBuilder,
this.onItemMoved,
this.onItemReordered,
this.theme,
super.key,
});

@override
State<ImpaktfullUiKanbanBoard<T>> createState() =>
_ImpaktfullUiKanbanBoardState<T>();
}

class _ImpaktfullUiKanbanBoardState<T>
extends State<ImpaktfullUiKanbanBoard<T>> {
List<ImpaktfullUiKanbanBoardItem<T>> _getItemsForColumn(String columnId) =>
widget.items.where((item) => item.columnId == columnId).toList();

void _handleItemDropped(
ImpaktfullUiKanbanBoardItem<T> item,
String targetColumnId,
int targetIndex,
) {
if (item.columnId == targetColumnId) return;
widget.onItemMoved?.call(
item,
item.columnId,
targetColumnId,
targetIndex,
);
}

void _handleItemReordered(
ImpaktfullUiKanbanBoardItem<T> item,
int newIndex,
String columnId,
) {
final columnItems = _getItemsForColumn(columnId);
final oldIndex = columnItems.indexWhere((i) => i.id == item.id);
if (oldIndex != -1 && oldIndex != newIndex) {
widget.onItemReordered?.call(item, oldIndex, newIndex);
}
}

@override
Widget build(BuildContext context) {
return ImpaktfullUiOverridableComponentBuilder(
component: widget,
overrideComponentTheme: widget.theme,
builder: (context, componentTheme) => LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: componentTheme.dimens.boardPadding,
child: ImpaktfullUiAutoLayout.horizontal(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: componentTheme.dimens.columnSpacing,
children: [
for (final column in widget.columns) ...[
SizedBox(
height: constraints.maxHeight,
child: ImpaktfullUiKanbanBoardColumn<T>(
config: column,
items: _getItemsForColumn(column.id),
itemBuilder: widget.itemBuilder,
theme: widget.theme,
onItemReordered: (item, newIndex) {
_handleItemReordered(item, newIndex, column.id);
},
onItemDropped: (item, targetIndex) {
_handleItemDropped(item, column.id, targetIndex);
},
),
),
],
],
),
),
),
);
}
}
Loading