feat: Add ImpaktfullUiKanbanBoard component#46
Conversation
…UI library. Here's a summary of what was created:
## Kanban Board Component
### Files Created:
**Models:**
- `lib/src/components/kanban_board/model/kanban_board_column_config.dart` - Column configuration with id, name, color, and index
- `lib/src/components/kanban_board/model/kanban_board_item.dart` - Board item with id, columnId, title, description, imageUrl, and generic data
**Components:**
- `lib/src/components/kanban_board/kanban_board.dart` - Main kanban board widget
- `lib/src/components/kanban_board/kanban_board.describe.dart` - Describe file for debugging
- `lib/src/components/kanban_board/kanban_board_column.dart` - Column component with drag-and-drop target
- `lib/src/components/kanban_board/kanban_board_column.describe.dart` - Describe file
- `lib/src/components/kanban_board/kanban_board_card.dart` - Card component with image and text
- `lib/src/components/kanban_board/kanban_board_card.describe.dart` - Describe file
- `lib/src/components/kanban_board/kanban_board_style.dart` - Theme with colors, dimensions, text styles, and shadows
### Features:
1. **Configurable Columns** - Each column has a name, color (displayed in header), and index for ordering
2. **Draggable Items** - Items can be dragged between columns using `LongPressDraggable` and `DragTarget`
3. **Reorderable within Column** - Items can be reordered within a column using `ReorderableListView`
4. **Card with Image** - Each kanban card supports title, description, and optional image
5. **Fully Themeable** - Follows the library's theming pattern with customizable colors, dimensions, text styles, and shadows
6. **Generic Data Support** - Items can hold custom data of any type via generics
### Usage Example:
```dart
ImpaktfullUiKanbanBoard<MyData>(
columns: [
ImpaktfullUiKanbanBoardColumnConfig(
id: 'todo',
name: 'To Do',
color: Colors.blue,
index: 0,
),
ImpaktfullUiKanbanBoardColumnConfig(
id: 'done',
name: 'Done',
color: Colors.green,
index: 1,
),
],
items: [
ImpaktfullUiKanbanBoardItem(
id: '1',
columnId: 'todo',
title: 'Task 1',
description: 'Description here',
imageUrl: 'https://example.com/image.jpg',
),
],
onItemMoved: (item, fromColumn, toColumn, newIndex) {
// Handle item moved between columns
},
onItemReordered: (item, oldIndex, newIndex) {
// Handle item reordered within column
},
)
```
## Summary I've created a complete Kanban Board component following the library's patterns and updated based on the CLAUDE.md guidelines. ### Files Created: - `lib/src/components/kanban_board/kanban_board.dart` - Main board widget - `lib/src/components/kanban_board/kanban_board.describe.dart` - Describe mixin - `lib/src/components/kanban_board/kanban_board_column.dart` - Column widget with drag target - `lib/src/components/kanban_board/kanban_board_column.describe.dart` - Describe mixin - `lib/src/components/kanban_board/kanban_board_card.dart` - Card widget with image support - `lib/src/components/kanban_board/kanban_board_card.describe.dart` - Describe mixin - `lib/src/components/kanban_board/kanban_board_style.dart` - Theme classes - `lib/src/components/kanban_board/model/kanban_board_column_config.dart` - Column config model - `lib/src/components/kanban_board/model/kanban_board_item.dart` - Item model ### Files Modified: - `lib/src/theme/component_theme.dart` - Added kanban board theme registration - `lib/src/theme/theme_default.dart` - Added default kanban board theme - `lib/impaktfull_ui.dart` - Added kanban board export - `CHANGELOG.md` - Added version 0.77.0 with new feature ### Features: 1. **Configurable Columns** - name, color, index 2. **Drag-and-Drop** - Items can be dragged between columns 3. **Reorderable** - Items can be reordered within columns 4. **Cards with Images** - Title, description, and optional image 5. **Fully Themeable** - Colors, dimensions, text styles, shadows 6. **Generic Data** - Items support custom data types via generics
- Resolved merge conflicts with main branch - Added AssetsTheme to kanban board style (following skill guidelines) - Added kanban board to example component library - Added kanban board to README.md component list - All formatting and analysis passes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request adds a new kanban board component (ImpaktfullUiKanbanBoard) to the UI library, providing drag-and-drop functionality for organizing items across configurable columns. The implementation includes comprehensive theming support and follows the existing component architecture patterns in the codebase.
Changes:
- Added kanban board component with configurable columns (name, color, index) and drag-and-drop support
- Created model classes for board items and column configuration
- Integrated complete theming support including colors, dimensions, text styles, and shadows
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/src/components/kanban_board/kanban_board.dart | Main kanban board component with state management for item movement and reordering |
| lib/src/components/kanban_board/kanban_board_column.dart | Column component implementing drag target and reorderable list |
| lib/src/components/kanban_board/kanban_board_card.dart | Card component displaying item title, description, and optional image |
| lib/src/components/kanban_board/model/kanban_board_item.dart | Model class for kanban board items with generic data support |
| lib/src/components/kanban_board/model/kanban_board_column_config.dart | Model class for column configuration |
| lib/src/components/kanban_board/kanban_board_style.dart | Complete theme definitions for the kanban board component |
| lib/src/components/kanban_board/*.describe.dart | Component descriptor files for debugging support |
| lib/src/theme/theme_default.dart | Integration of kanban board theme into default theme |
| lib/src/theme/component_theme.dart | Registration of kanban board theme in component theme system |
| lib/impaktfull_ui.dart | Public export of kanban board component |
| example/pubspec.lock | Updated dependency versions and Flutter SDK requirements |
| example/macos/Podfile | Updated macOS deployment target to 10.15 |
| example/macos/Runner.xcodeproj/project.pbxproj | Updated macOS deployment target in Xcode project |
| example/macos/Podfile.lock | Updated pod checksums |
| CHANGELOG.md | Added release notes for version 0.77.0 |
| CLAUDE.md | Empty file added |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| List<ImpaktfullUiKanbanBoardColumnConfig> get _sortedColumns { | ||
| final sorted = | ||
| List<ImpaktfullUiKanbanBoardColumnConfig>.from(widget.columns); | ||
| sorted.sort((a, b) => a.index.compareTo(b.index)); | ||
| return sorted; | ||
| } |
There was a problem hiding this comment.
The _sortedColumns getter creates a new list and sorts it on every access. Since it's accessed multiple times in the build method (lines 107, 114, 120, 123), this could impact performance with many columns. Consider computing the sorted columns once, either by caching the result or making it a late final variable that's computed once.
| void _handleItemDropped( | ||
| ImpaktfullUiKanbanBoardItem<T> item, | ||
| String targetColumnId, | ||
| ) { | ||
| if (item.columnId != targetColumnId) { | ||
| final targetColumnItems = _getItemsForColumn(targetColumnId); | ||
| widget.onItemMoved?.call( | ||
| item, | ||
| item.columnId, | ||
| targetColumnId, | ||
| targetColumnItems.length, | ||
| ); | ||
| } |
There was a problem hiding this comment.
The onItemMoved callback receives item.columnId as the fromColumnId parameter (line 74), but at this point the item still belongs to the old column. This is correct behavior, but the API design is potentially confusing. Consider adding a clarifying comment or renaming the parameter in the item model to make it clearer that columnId represents the current/from column when the callback is invoked.
| import 'package:flutter/material.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/util/descriptor/component_descriptor_mixin.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'; | ||
|
|
||
| part 'kanban_board.describe.dart'; | ||
|
|
||
| class ImpaktfullUiKanbanBoard<T> extends StatefulWidget | ||
| with ComponentDescriptorMixin { | ||
| 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>(); | ||
|
|
||
| @override | ||
| String describe(BuildContext context) => _describeInstance(context, this); | ||
| } | ||
|
|
||
| class _ImpaktfullUiKanbanBoardState<T> | ||
| extends State<ImpaktfullUiKanbanBoard<T>> { | ||
| List<ImpaktfullUiKanbanBoardColumnConfig> get _sortedColumns { | ||
| final sorted = | ||
| List<ImpaktfullUiKanbanBoardColumnConfig>.from(widget.columns); | ||
| sorted.sort((a, b) => a.index.compareTo(b.index)); | ||
| return sorted; | ||
| } | ||
|
|
||
| List<ImpaktfullUiKanbanBoardItem<T>> _getItemsForColumn(String columnId) { | ||
| return widget.items.where((item) => item.columnId == columnId).toList(); | ||
| } | ||
|
|
||
| void _handleItemDropped( | ||
| ImpaktfullUiKanbanBoardItem<T> item, | ||
| String targetColumnId, | ||
| ) { | ||
| if (item.columnId != targetColumnId) { | ||
| final targetColumnItems = _getItemsForColumn(targetColumnId); | ||
| widget.onItemMoved?.call( | ||
| item, | ||
| item.columnId, | ||
| targetColumnId, | ||
| targetColumnItems.length, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| 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) { | ||
| return LayoutBuilder( | ||
| builder: (context, constraints) { | ||
| return SingleChildScrollView( | ||
| scrollDirection: Axis.horizontal, | ||
| padding: componentTheme.dimens.boardPadding, | ||
| child: Row( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| for (var i = 0; i < _sortedColumns.length; i++) ...[ | ||
| if (i > 0) | ||
| SizedBox(width: componentTheme.dimens.columnSpacing), | ||
| SizedBox( | ||
| height: constraints.maxHeight - | ||
| componentTheme.dimens.boardPadding.vertical, | ||
| child: ImpaktfullUiKanbanBoardColumn<T>( | ||
| config: _sortedColumns[i], | ||
| items: _getItemsForColumn(_sortedColumns[i].id), | ||
| itemBuilder: widget.itemBuilder, | ||
| theme: widget.theme, | ||
| onItemReordered: (item, newIndex) { | ||
| _handleItemReordered( | ||
| item, newIndex, _sortedColumns[i].id); | ||
| }, | ||
| onItemDropped: (item) { | ||
| _handleItemDropped(item, _sortedColumns[i].id); | ||
| }, | ||
| ), | ||
| ), | ||
| ], | ||
| ], | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
| }, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new kanban board component lacks test coverage. Given that the repository contains golden tests for other components (e.g., button tests in test/src/components/button/), the kanban board should also have corresponding test coverage to ensure functionality and visual consistency.
The previous implementation used ReorderableListView which captured drag gestures and prevented cross-column dragging. This change: - Replaced ReorderableListView with regular ListView + DragTarget - Each column is now a DragTarget that accepts items from any column - Added visual drop indicator showing where items will be placed - Items can now be dragged between columns and dropped at specific positions - Within-column reordering still works via the same drag mechanism Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Summary of Changes I've addressed all three issues you identified: ### 1. Card Subtitle Styling (Fixed) - Updated `kanban_board_style.dart` to use the correct text style pattern - Changed from `textStyles.onCardSecondary.text.small` to `textStyles.onCard.text.small.light` - This matches how other components like `SimpleListItem` handle subtitle styling - Also updated `cardTitle` to use `textStyles.onCard.text.small.medium` for consistency ### 2. Card Image Display (Fixed) - The issue was passing `double.infinity` as width to `ImpaktfullUiNetworkImage` - When the network image component handles picsum.photos URLs, it tries to convert width to int, causing issues with infinity - Fixed by wrapping the image in `LayoutBuilder` to get the actual constraint width ### 3. Example State Changes (Fixed) - Created a new `KanbanBoardStateConfigurator` widget at `example/lib/src/component_library/items/kanban_board/widget/kanban_board_state_configurator.dart` - This StatefulWidget manages the kanban items list and properly handles: - `onItemMoved`: Updates the item's columnId when moved between columns - `onItemReordered`: Reorders items within the same column - Updated `kanban_board_library_variant.dart` to use this stateful configurator The code passes both format and analyze checks.
- Increased the height of the kanban board to 850 for better visibility. - Simplified the builder function in `KanbanBoardStateConfigurator` for improved readability. - Updated the image URL handling in `kanban_board_state_configurator.dart` to remove unnecessary dimensions. - Refactored `kanban_board_card.dart` to utilize `ImpaktfullUiAutoLayout` for better layout management and removed the descriptor mixin. - Streamlined the column widget in `kanban_board_column.dart` to use `ImpaktfullUiAutoLayout` for vertical and horizontal arrangements, enhancing layout consistency. - Removed unused descriptor files for `kanban_board_card` and `kanban_board_column` to clean up the codebase. These changes improve the overall structure and maintainability of the kanban board component.
Summary
ImpaktfullUiKanbanBoardcomponent with full drag-and-drop supportChanges Made
New Components
New Models
Theme Support
ImpaktfullUiComponentsThemeFeatures
LongPressDraggableandDragTargetReorderableListViewUsage Example
Test Plan
dart formatThis PR was written using Vibe Kanban