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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<BbSortable TItem="string"
Items="@_items"
AriaLabel="Sortable task list"
OnUpdate="HandleUpdate">
<ItemTemplate Context="item">
<div class="flex items-center gap-3 rounded-md border bg-card px-4 py-3 text-sm shadow-sm cursor-grab active:cursor-grabbing">
<LucideIcon Name="grip-vertical" Size="16" Class="text-muted-foreground shrink-0" />
<span>@item</span>
</div>
</ItemTemplate>
</BbSortable>

@code {
private List<string> _items =
[
"Design mockups",
"Set up project",
"Implement authentication",
"Write unit tests",
"Deploy to staging",
];

private void HandleUpdate((int OldIndex, int NewIndex) args)
{
var item = _items[args.OldIndex];
_items.RemoveAt(args.OldIndex);
_items.Insert(args.NewIndex, item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@* Layout="SortableLayout.Grid" applies a two-column grid. *@
@* Override columns via Class, e.g., Class="grid-cols-3 gap-3". *@

<BbSortable TItem="ColorCard"
Items="@_cards"
Layout="SortableLayout.Grid"
AriaLabel="Sortable card grid"
OnUpdate="HandleUpdate">
<ItemTemplate Context="card">
@* Wrap Razor components in a <div> so Sortable.js tracks a single DOM node. *@
<div>
<BbCard Class="cursor-grab active:cursor-grabbing select-none">
<BbCardContent Class="pt-6">
<div class="@($"h-16 rounded-md {card.Color} flex items-center justify-center")">
<span class="text-white font-semibold text-sm">@card.Label</span>
</div>
</BbCardContent>
</BbCard>
</div>
</ItemTemplate>
</BbSortable>

@code {
private record ColorCard(string Label, string Color);

private List<ColorCard> _cards =
[
new("Ocean", "bg-blue-500"),
new("Forest", "bg-green-500"),
new("Sunset", "bg-orange-500"),
new("Berry", "bg-purple-500"),
new("Rose", "bg-rose-500"),
new("Sky", "bg-cyan-500"),
];

private void HandleUpdate((int OldIndex, int NewIndex) args)
{
var card = _cards[args.OldIndex];
_cards.RemoveAt(args.OldIndex);
_cards.Insert(args.NewIndex, card);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<BbSortable TItem="string"
Items="@_items"
Handle=".drag-handle"
AriaLabel="Sortable list with drag handle"
OnUpdate="HandleUpdate">
<ItemTemplate Context="item">
<div class="flex items-center gap-3 rounded-md border bg-card px-4 py-3 text-sm shadow-sm">
<span class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground">
<LucideIcon Name="grip-vertical" Size="16" />
</span>
<span class="flex-1">@item</span>
</div>
</ItemTemplate>
</BbSortable>

@code {
private List<string> _items =
[
"Dashboard",
"Analytics",
"Reports",
"Settings",
"Users",
];

private void HandleUpdate((int OldIndex, int NewIndex) args)
{
var item = _items[args.OldIndex];
_items.RemoveAt(args.OldIndex);
_items.Insert(args.NewIndex, item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
@* Kanban board — three connected columns sharing the same Group name. *@
@* OnRemove fires on the source column; OnAdd fires on the target column. *@
@* The parent stages the dragged item in _draggedTask between the two callbacks. *@

<div class="grid grid-cols-3 gap-4">
@foreach (var column in _columns)
{
<div class="space-y-2">
<div class="flex items-center justify-between">
<p class="text-sm font-semibold">@column.Title</p>
<BbBadge Variant="BadgeVariant.Secondary">@column.Items.Count</BbBadge>
</div>
<BbSortable TItem="KanbanTask"
Items="@column.Items"
Group="kanban"
AriaLabel="@($"{column.Title} column")"
OnUpdate="(args) => MoveWithin(column.Items, args)"
OnRemove="(args) => OnKanbanRemove(column, args)"
OnAdd="(args) => OnKanbanAdd(column, args)"
Class="min-h-24 rounded-lg border border-dashed p-2">
<ItemTemplate Context="task">
@* Wrap BbCard in a div so Sortable.js tracks a single DOM node. *@
<div class="mb-2 last:mb-0">
<BbCard Class="cursor-grab active:cursor-grabbing">
<BbCardHeader Class="pb-2">
<BbCardTitle Class="text-sm font-medium">@task.Title</BbCardTitle>
</BbCardHeader>
<BbCardContent>
<BbBadge Variant="@GetPriorityVariant(task.Priority)">
@task.Priority
</BbBadge>
</BbCardContent>
</BbCard>
</div>
</ItemTemplate>
</BbSortable>
</div>
}
</div>

@code {
private record KanbanTask(string Title, string Priority);

private class KanbanColumn(string title, List<KanbanTask> items)
{
public string Title { get; } = title;
public List<KanbanTask> Items { get; } = items;
}

private List<KanbanColumn> _columns =
[
new("To Do",
[
new("Design system audit", "High"),
new("Update README", "Low"),
new("Fix login bug", "High"),
]),
new("In Progress",
[
new("Build sortable demo", "Medium"),
new("Write unit tests", "Medium"),
]),
new("Done",
[
new("Project setup", "Low"),
new("CI pipeline", "Medium"),
]),
];

// Stages the task that is currently being dragged across columns.
// OnRemove (source) populates it; OnAdd (target) consumes it.
private KanbanTask? _draggedTask;

private void MoveWithin(List<KanbanTask> list, (int OldIndex, int NewIndex) args)
{
var task = list[args.OldIndex];
list.RemoveAt(args.OldIndex);
list.Insert(args.NewIndex, task);
}

private void OnKanbanRemove(KanbanColumn source, (int OldIndex, int NewIndex) args)
{
_draggedTask = source.Items[args.OldIndex];
source.Items.RemoveAt(args.OldIndex);
}

private void OnKanbanAdd(KanbanColumn target, (int OldIndex, int NewIndex) args)
{
if (_draggedTask is null)
{
return;
}

target.Items.Insert(Math.Min(args.NewIndex, target.Items.Count), _draggedTask);
_draggedTask = null;
}

private static BadgeVariant GetPriorityVariant(string priority) => priority switch
{
"High" => BadgeVariant.Destructive,
"Medium" => BadgeVariant.Default,
_ => BadgeVariant.Secondary,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@* Items can be dragged between the two lists. Both lists share the same Group name. *@

<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<p class="text-sm font-medium">Available</p>
<BbSortable TItem="string"
Items="@_available"
Group="shared-pool"
AriaLabel="Available items"
OnUpdate="(args) => MoveWithin(_available, args)"
OnRemove="(args) => MoveAcross(_available, _selected, args)"
Class="min-h-20 rounded-md border border-dashed p-2">
<ItemTemplate Context="item">
<div class="flex items-center gap-2 rounded-md border bg-card px-3 py-2 text-sm shadow-sm cursor-grab active:cursor-grabbing">
<LucideIcon Name="grip-vertical" Size="14" Class="text-muted-foreground shrink-0" />
<span>@item</span>
</div>
</ItemTemplate>
</BbSortable>
</div>

<div class="space-y-2">
<p class="text-sm font-medium">Selected</p>
<BbSortable TItem="string"
Items="@_selected"
Group="shared-pool"
AriaLabel="Selected items"
OnUpdate="(args) => MoveWithin(_selected, args)"
OnRemove="(args) => MoveAcross(_selected, _available, args)"
Class="min-h-20 rounded-md border border-dashed p-2">
<ItemTemplate Context="item">
<div class="flex items-center gap-2 rounded-md border bg-card px-3 py-2 text-sm shadow-sm cursor-grab active:cursor-grabbing">
<LucideIcon Name="grip-vertical" Size="14" Class="text-muted-foreground shrink-0" />
<span>@item</span>
</div>
</ItemTemplate>
</BbSortable>
</div>
</div>

@code {
private List<string> _available = ["React", "Vue", "Angular", "Svelte", "Solid"];
private List<string> _selected = ["Blazor"];

private static void MoveWithin(List<string> list, (int OldIndex, int NewIndex) args)
{
var item = list[args.OldIndex];
list.RemoveAt(args.OldIndex);
list.Insert(args.NewIndex, item);
}

private static void MoveAcross(List<string> source, List<string> target, (int OldIndex, int NewIndex) args)
{
var item = source[args.OldIndex];
source.RemoveAt(args.OldIndex);
target.Insert(args.NewIndex, item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,14 @@
</p>
</a>

<!-- Sortable -->
<a href="/components/sortable" class="group p-5 border rounded-lg hover:border-primary hover:bg-accent transition-colors no-underline">
<h3 class="font-semibold text-lg mb-2 group-hover:text-primary">Sortable</h3>
<p class="text-sm text-muted-foreground">
Drag-and-drop sortable lists, grids, and Kanban boards
</p>
</a>

<!-- Skeleton -->
<a href="/components/skeleton" class="group p-5 border rounded-lg hover:border-primary hover:bg-accent transition-colors no-underline">
<h3 class="font-semibold text-lg mb-2 group-hover:text-primary">Skeleton</h3>
Expand Down
Loading