Expand the Sortable demo and add default SortableJS drag styles (#3106)

This commit is contained in:
Bartosz-Do authored and GitHub committed 2026-09-21 14:13:22 +02:00
1 parent ce523643b2
commit bd60d5fd4c
11 files changed
+602 -11

No files matched your search

+2
View File
@@ -36,6 +36,8 @@ sort-score
sort-status
sort-tags
sort-type
sortable-fixed
sortable-handle
table-tbody
tblr-illustrations-computer-fix-a
tblr-illustrations-computer-fix-b
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": minor
---
Added grouped Sortable demos: drag handle, table rows, kanban board, clone, multi-drag and swap.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": minor
---
Added SortableJS drag styles and turned on `forceFallback` by default so `data-sortable` lists drag a styled, tilted copy.
+4 -1
View File
@@ -22,7 +22,9 @@ const DATA_ATTRIBUTE = `data-${NAME}`
const SELECTOR_DATA_SORTABLE = `[${DATA_ATTRIBUTE}]`
const Default: ComponentConfig = {}
const Default: ComponentConfig = {
forceFallback: true,
}
const DefaultType: Record<string, string> = {}
@@ -32,6 +34,7 @@ const DefaultType: Record<string, string> = {}
* Wraps SortableJS (https://sortablejs.github.io/Sortable/), loaded separately
* as `window.Sortable`. Without the plugin the component is inert. Options come
* from the `data-sortable` attribute as JSON, or from the config object.
* `forceFallback` is on by default so the dragged copy can be styled with `.sortable-drag`.
*/
class Sortable extends BaseComponent {
+13
View File
@@ -45,6 +45,19 @@ describe('Sortable', () => {
expect(instance.sortable).toBe(plugin.mock.instances[0])
})
it('should drag a styled copy by default', () => {
new Sortable(list())
expect(plugin.mock.calls[0][1]).toMatchObject({ forceFallback: true })
})
it('should let data-sortable turn the styled copy off', () => {
fixtureEl.innerHTML = '<ul data-sortable=\'{"forceFallback":false}\'><li>a</li></ul>'
new Sortable(list())
expect(plugin.mock.calls[0][1]).toMatchObject({ forceFallback: false })
})
it('should let the config object win over the attribute', () => {
new Sortable(list(), { animation: 0, group: 'shared' })
+4 -4
View File
@@ -82,19 +82,19 @@
"files": [
{
"path": "./dist/css/tabler.css",
"maxSize": "87 kB"
"maxSize": "88 kB"
},
{
"path": "./dist/css/tabler.min.css",
"maxSize": "81 kB"
"maxSize": "82 kB"
},
{
"path": "./dist/css/tabler.rtl.css",
"maxSize": "87 kB"
"maxSize": "88 kB"
},
{
"path": "./dist/css/tabler.rtl.min.css",
"maxSize": "81 kB"
"maxSize": "82 kB"
},
{
"path": "./dist/css/tabler-flags.css",
+1
View File
@@ -101,6 +101,7 @@
@forward 'ui/chat';
@forward 'ui/signature';
@forward 'ui/patterns';
@forward 'ui/sortable';
@forward 'helpers/index';
+12
View File
@@ -1837,6 +1837,18 @@ $tracking-border-radius: var(--border-radius) !default;
$tracking-block-min-width: 0.25rem !default;
// scss-docs-end tracking-variables
// Sortable
// scss-docs-start sortable-variables
$sortable-ghost-bg: var(--primary-lt) !default;
$sortable-ghost-border-color: var(--primary) !default;
$sortable-ghost-opacity: 0.6 !default;
$sortable-outline-width: 1px !default;
$sortable-selected-bg: var(--primary-lt) !default;
$sortable-drag-bg: var(--bg-surface) !default;
$sortable-drag-rotate: 1.5deg !default;
$sortable-drag-box-shadow: $box-shadow-lg !default;
// scss-docs-end sortable-variables
// Progress
// scss-docs-start progress-variables
$progress-height: 0.5rem !default;
+73
View File
@@ -0,0 +1,73 @@
//
// Sortable
//
@use '../config' as *;
.sortable-ghost,
.sortable-swap-highlight,
.sortable-selected,
.sortable-drag {
--sortable-ghost-bg: #{$sortable-ghost-bg};
--sortable-ghost-border-color: #{$sortable-ghost-border-color};
--sortable-ghost-opacity: #{$sortable-ghost-opacity};
--sortable-outline-width: #{$sortable-outline-width};
--sortable-selected-bg: #{$sortable-selected-bg};
--sortable-drag-bg: #{$sortable-drag-bg};
--sortable-drag-rotate: #{$sortable-drag-rotate};
--sortable-drag-box-shadow: #{$sortable-drag-box-shadow};
}
.sortable-ghost,
.sortable-swap-highlight {
background-color: var(--sortable-ghost-bg);
outline: var(--sortable-outline-width) dashed var(--sortable-ghost-border-color);
outline-offset: calc(var(--sortable-outline-width) * -1);
}
.sortable-ghost {
opacity: var(--sortable-ghost-opacity);
}
tr.sortable-ghost,
tr.sortable-swap-highlight,
tr.sortable-selected {
> * {
--table-bg-state: var(--sortable-ghost-bg);
}
}
.sortable-selected {
background-color: var(--sortable-selected-bg);
outline: var(--sortable-outline-width) solid var(--sortable-ghost-border-color);
outline-offset: calc(var(--sortable-outline-width) * -1);
}
.list-group-item {
&.sortable-ghost,
&.sortable-swap-highlight,
&.sortable-selected,
&.sortable-drag {
@include border-radius(var(--list-group-border-radius));
}
}
.sortable-chosen {
cursor: grabbing;
}
.sortable-drag {
cursor: grabbing;
background-color: var(--sortable-drag-bg);
outline: var(--sortable-outline-width) solid var(--sortable-ghost-border-color);
outline-offset: calc(var(--sortable-outline-width) * -1);
box-shadow: var(--sortable-drag-box-shadow);
opacity: 1 !important; // stylelint-disable-line declaration-no-important
transition: none;
/* rtl:ignore */
rotate: calc(var(--sortable-drag-rotate) * var(--dir));
}
.card.sortable-drag {
box-shadow: var(--sortable-drag-box-shadow);
}
+396 -6
View File
@@ -1,21 +1,65 @@
---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Avatar from '@ui/Avatar.astro'
import Card from '@ui/Card.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardBody from '@ui/CardBody.astro'
import CardTitle from '@ui/CardTitle.astro'
import CardSubtitle from '@ui/CardSubtitle.astro'
import Icon from '@ui/Icon.astro'
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
import Signal from '@ui/Signal.astro'
import { people, personById } from '@shared/lib/people'
import tasks from '@data/tasks.json'
interface Task {
name: string
assigned_to: number
priority: string
}
interface Column {
name: string
tasks: Task[]
}
const priorities: Record<string, { value: number; color: string }> = {
High: { value: 3, color: 'red' },
Medium: { value: 2, color: 'orange' },
Low: { value: 1, color: 'blue' },
}
const board = (tasks.columns as Column[]).map((column) => ({
name: column.name,
tasks: column.tasks.slice(0, 4),
}))
const selectedTopics = ['Design', 'Development', 'Marketing']
const availableTopics = ['Sales', 'Support', 'Analytics', 'Legal', 'Finance', 'Research', 'Security', 'Hiring']
const team = people.slice(0, 5)
const gallery = people.slice(5, 11)
const sections = [
{ title: 'Basics', description: 'Add data-sortable with SortableJS options as JSON to any list to make its children draggable.' },
{ title: 'Handles and tables', description: 'Limit dragging to a grip icon, or reorder the rows of a table.' },
{ title: 'Boards', description: 'Connect several lists to build a kanban board, a pick list or a nested tree.' },
{ title: 'Selection and swapping', description: 'The bundled SortableJS build also ships the MultiDrag and Swap plugins.' },
]
---
<DefaultLayout title="Sortable" pageHeader="SortableJS" pageMenu="plugins.sortable" pageLibs={['sortablejs']}>
<DefaultLayout title="Sortable" pageMenu="plugins.sortable" pageLibs={['sortablejs']} description="Drag and drop to reorder lists, table rows, grids and kanban boards with SortableJS.">
<div class="row row-cards">
<div class="col-12">
<h2 class="page-section-title">{sections[0]!.title}</h2>
<p class="page-section-description">{sections[0]!.description}</p>
</div>
<div class="col-md-6">
<Card>
<Card class="h-100">
<CardHeader>
<div>
<CardTitle>Basic usage</CardTitle>
<CardTitle as="h3">Basic usage</CardTitle>
<CardSubtitle>Drag to reorder items within a single list.</CardSubtitle>
</div>
</CardHeader>
@@ -30,11 +74,11 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
</Card>
</div>
<div class="col-md-6">
<Card>
<Card class="h-100">
<CardHeader>
<div>
<CardTitle>Two lists with drag and drop</CardTitle>
<CardSubtitle>Move items between two connected lists.</CardSubtitle>
<CardTitle as="h3">Two lists with drag and drop</CardTitle>
<CardSubtitle>Give lists the same <code>group</code> to move items between them.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
@@ -57,5 +101,351 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Horizontal lists</CardTitle>
<CardSubtitle>Set <code>direction</code> to <code>horizontal</code> for rows of tags, and drag them between the two rows.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<div class="subheader mb-2">Selected</div>
<div class="tag-list mb-4" data-sortable='{"group":"topics","animation":150,"direction":"horizontal","emptyInsertThreshold":40}'>
{selectedTopics.map((topic) => <span class="tag cursor-move">{topic}</span>)}
</div>
<div class="subheader mb-2">Available</div>
<div class="tag-list" data-sortable='{"group":"topics","animation":150,"direction":"horizontal","emptyInsertThreshold":40}'>
{availableTopics.map((topic) => <span class="tag cursor-move">{topic}</span>)}
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Fixed items</CardTitle>
<CardSubtitle>Set <code>filter</code> to a selector to make matching items impossible to drag.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<ListGroup as="ul" data-sortable='{"animation":150,"filter":".sortable-fixed"}'>
<ListGroupItem as="li" class="sortable-fixed text-secondary d-flex align-items-center gap-2">
<Icon name="lock" /> Inbox (pinned)
</ListGroupItem>
<ListGroupItem as="li">Projects</ListGroupItem>
<ListGroupItem as="li">Reports</ListGroupItem>
<ListGroupItem as="li">Archive</ListGroupItem>
<ListGroupItem as="li" class="sortable-fixed text-secondary d-flex align-items-center gap-2">
<Icon name="lock" /> Trash (pinned)
</ListGroupItem>
</ListGroup>
</CardBody>
</Card>
</div>
<div class="col-12">
<h2 class="page-section-title">{sections[1]!.title}</h2>
<p class="page-section-description">{sections[1]!.description}</p>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Drag handle</CardTitle>
<CardSubtitle>Set <code>handle</code> so only the grip starts a drag, and the rest of the row stays clickable.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<ListGroup as="ul" data-sortable='{"animation":150,"handle":".sortable-handle"}'>
{
team.map((person) => (
<ListGroupItem as="li">
<div class="row align-items-center g-3">
<div class="col-auto">
<span class="sortable-handle cursor-move text-secondary" aria-hidden="true">
<Icon name="grip-vertical" />
</span>
</div>
<div class="col-auto">
<Avatar person={person} />
</div>
<div class="col text-truncate">
<a href="#" class="text-reset d-block text-truncate">
{person.full_name}
</a>
<div class="text-secondary text-truncate">{person.job_title}</div>
</div>
</div>
</ListGroupItem>
))
}
</ListGroup>
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Sortable table</CardTitle>
<CardSubtitle>Put <code>data-sortable</code> on the <code>tbody</code> to reorder rows.</CardSubtitle>
</div>
</CardHeader>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th class="w-1"><span class="visually-hidden">Drag</span></th>
<th>Name</th>
<th>Title</th>
<th>Country</th>
</tr>
</thead>
<tbody data-sortable='{"animation":150,"handle":".sortable-handle"}'>
{
team.map((person) => (
<tr>
<td>
<span class="sortable-handle cursor-move text-secondary" aria-hidden="true">
<Icon name="grip-vertical" />
</span>
</td>
<td>{person.full_name}</td>
<td class="text-secondary">{person.job_title}</td>
<td class="text-secondary">{person.country}</td>
</tr>
))
}
</tbody>
</table>
</div>
</Card>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Grid of avatars</CardTitle>
<CardSubtitle>Sortable also works on a wrapping layout, not just on vertical lists.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<div class="d-flex flex-wrap gap-3" data-sortable='{"animation":150}'>
{
gallery.map((person) => (
<div class="card card-sm cursor-move flex-fill">
<div class="card-body text-center">
<Avatar person={person} size="lg" class="mb-2" />
<div class="text-truncate">{person.first_name}</div>
</div>
</div>
))
}
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Disabled list</CardTitle>
<CardSubtitle>Set <code>disabled</code> to <code>true</code> to lock a list, for example while the user has no edit rights.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<ListGroup as="ul" data-sortable='{"animation":150,"disabled":true}'>
<ListGroupItem as="li" class="d-flex align-items-center gap-2">
<Icon name="lock" class="text-secondary" /> Read-only item 1
</ListGroupItem>
<ListGroupItem as="li" class="d-flex align-items-center gap-2">
<Icon name="lock" class="text-secondary" /> Read-only item 2
</ListGroupItem>
<ListGroupItem as="li" class="d-flex align-items-center gap-2">
<Icon name="lock" class="text-secondary" /> Read-only item 3
</ListGroupItem>
</ListGroup>
</CardBody>
</Card>
</div>
<div class="col-12">
<h2 class="page-section-title">{sections[2]!.title}</h2>
<p class="page-section-description">{sections[2]!.description}</p>
</div>
<div class="col-12">
<Card>
<CardHeader>
<div>
<CardTitle as="h3">Kanban board</CardTitle>
<CardSubtitle>Drag tasks between columns that share one <code>group</code>. Items are dragged as a tilted copy with a shadow. Set <code>forceFallback</code> to <code>false</code> to use the browser preview instead.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<div class="row g-3">
{
board.map((column) => (
<div class="col-md-4">
<h4 class="mb-2">{column.name}</h4>
<div class="d-flex flex-column gap-2" data-sortable='{"group":"kanban","animation":150,"emptyInsertThreshold":40}'>
{column.tasks.map((task) => (
<div class="card card-sm cursor-move">
<div class="card-body">
<div class="mb-3">{task.name}</div>
<div class="d-flex align-items-center justify-content-between">
<span class="d-flex align-items-center gap-2 text-secondary">
<Signal value={priorities[task.priority]?.value ?? 0} color={priorities[task.priority]?.color} />
{task.priority}
</span>
<Avatar person={personById(task.assigned_to)} size="xs" />
</div>
</div>
</div>
))}
</div>
</div>
))
}
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-xl-4">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Clone from a list</CardTitle>
<CardSubtitle>Use <code>pull: "clone"</code> and <code>put: false</code> on the source so items are copied, not moved.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<div class="row g-3">
<div class="col-sm-6">
<div class="subheader mb-2">Templates</div>
<ListGroup as="ul" data-sortable='{"group":{"name":"clone","pull":"clone","put":false},"sort":false,"animation":150}'>
<ListGroupItem as="li">Heading</ListGroupItem>
<ListGroupItem as="li">Paragraph</ListGroupItem>
<ListGroupItem as="li">Image</ListGroupItem>
<ListGroupItem as="li">Button</ListGroupItem>
</ListGroup>
</div>
<div class="col-sm-6">
<div class="subheader mb-2">Page</div>
<ListGroup as="ul" data-sortable='{"group":"clone","animation":150}'>
<ListGroupItem as="li">Paragraph</ListGroupItem>
</ListGroup>
</div>
</div>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-xl-4">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">One-way transfer</CardTitle>
<CardSubtitle>Set <code>put: false</code> on a list to make it a source only, and use it to build a pick list.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<div class="row g-3">
<div class="col-sm-6">
<div class="subheader mb-2">Available</div>
<ListGroup as="ul" data-sortable='{"group":{"name":"transfer","put":false},"sort":false,"animation":150}'>
<ListGroupItem as="li">Email alerts</ListGroupItem>
<ListGroupItem as="li">SMS alerts</ListGroupItem>
<ListGroupItem as="li">Weekly digest</ListGroupItem>
<ListGroupItem as="li">Push notifications</ListGroupItem>
</ListGroup>
</div>
<div class="col-sm-6">
<div class="subheader mb-2">Selected</div>
<ListGroup as="ul" data-sortable='{"group":"transfer","animation":150}'>
<ListGroupItem as="li">In-app messages</ListGroupItem>
</ListGroup>
</div>
</div>
</CardBody>
</Card>
</div>
<div class="col-12 col-xl-4">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Nested lists</CardTitle>
<CardSubtitle>Put a sortable list inside an item and give all lists one <code>group</code> to move items between levels.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<ListGroup as="ul" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
<ListGroupItem as="li">
<div>Design</div>
<ListGroup as="ul" class="mt-2" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
<ListGroupItem as="li">Wireframes</ListGroupItem>
<ListGroupItem as="li">Prototype</ListGroupItem>
</ListGroup>
</ListGroupItem>
<ListGroupItem as="li">
<div>Development</div>
<ListGroup as="ul" class="mt-2" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
<ListGroupItem as="li">Frontend</ListGroupItem>
<ListGroupItem as="li">Backend</ListGroupItem>
<ListGroupItem as="li">Tests</ListGroupItem>
</ListGroup>
</ListGroupItem>
<ListGroupItem as="li">
<div>Launch</div>
<ListGroup as="ul" class="mt-2" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
<ListGroupItem as="li">Release notes</ListGroupItem>
</ListGroup>
</ListGroupItem>
</ListGroup>
</CardBody>
</Card>
</div>
<div class="col-12">
<h2 class="page-section-title">{sections[3]!.title}</h2>
<p class="page-section-description">{sections[3]!.description}</p>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Multiple selection</CardTitle>
<CardSubtitle>Set <code>multiDrag</code> to <code>true</code>. Hold <kbd>Ctrl</kbd> or <kbd>⌘</kbd> and click to select items, then drag them together.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<ListGroup as="ul" data-sortable='{"multiDrag":true,"animation":150}'>
<ListGroupItem as="li">Invoice #1001</ListGroupItem>
<ListGroupItem as="li">Invoice #1002</ListGroupItem>
<ListGroupItem as="li">Invoice #1003</ListGroupItem>
<ListGroupItem as="li">Invoice #1004</ListGroupItem>
<ListGroupItem as="li">Invoice #1005</ListGroupItem>
</ListGroup>
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card class="h-100">
<CardHeader>
<div>
<CardTitle as="h3">Swap</CardTitle>
<CardSubtitle>Set <code>swap</code> to <code>true</code> to exchange two items instead of shifting the rest.</CardSubtitle>
</div>
</CardHeader>
<CardBody>
<ListGroup as="ul" data-sortable='{"swap":true,"animation":150}'>
<ListGroupItem as="li">First place</ListGroupItem>
<ListGroupItem as="li">Second place</ListGroupItem>
<ListGroupItem as="li">Third place</ListGroupItem>
<ListGroupItem as="li">Fourth place</ListGroupItem>
</ListGroup>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout>
+87
View File
@@ -0,0 +1,87 @@
---
// Sortable on a kanban board, drawn in the middle of a drag. The card being
// moved has left the Upcoming column and shows twice, as SortableJS renders it:
// a dashed slot in In Progress where it will land, and the copy that follows the
// pointer, lifted with a shadow and an outline. Both carry the real state classes
// (`.sortable-ghost`, `.sortable-drag`), so the picture shows the shipped styles.
// The fake cursor rests on the floating card.
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
import Avatar from '@ui/Avatar.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import Signal from '@ui/Signal.astro'
import { personById } from '@shared/lib/people'
import tasks from '@data/tasks.json'
interface Task {
name: string
assigned_to: number
priority: string
}
interface Column {
name: string
tasks: Task[]
}
const [upcoming, progress, completed] = tasks.columns as Column[] as [Column, Column, Column]
const priorities: Record<string, { value: number; color: string }> = {
High: { value: 3, color: 'red' },
Medium: { value: 2, color: 'orange' },
Low: { value: 1, color: 'blue' },
}
const moving = upcoming.tasks[2]!
const columns = [
{ name: upcoming.name, cards: [upcoming.tasks[0]!, upcoming.tasks[1]!] },
{ name: progress.name, cards: [progress.tasks[0]!, moving, progress.tasks[1]!] },
{ name: completed.name, cards: [completed.tasks[0]!, completed.tasks[1]!, completed.tasks[2]!] },
]
---
<ScreenshotLayout title="Sortable" columns={3}>
<Card>
<CardBody class="p-4 position-relative">
<div class="row g-3">
{
columns.map((column) => (
<div class="col-4">
<h4 class="mb-2">{column.name}</h4>
<div class="d-flex flex-column gap-2">
{column.cards.map((task) => (
<Card size="sm" class:list={['cursor-move', task === moving && 'sortable-chosen sortable-ghost']}>
<CardBody>
<div class="mb-3">{task.name}</div>
<div class="d-flex align-items-center justify-content-between">
<span class="d-flex align-items-center gap-2 text-secondary">
<Signal value={priorities[task.priority]?.value ?? 0} color={priorities[task.priority]?.color} />
{task.priority}
</span>
<Avatar person={personById(task.assigned_to)} size="xs" />
</div>
</CardBody>
</Card>
))}
</div>
</div>
))
}
</div>
<div class="card card-sm sortable-chosen sortable-fallback sortable-drag" style="position: absolute; top: 11rem; left: 39.5%; width: 28.5%">
<div class="card-body">
<div class="mb-3">{moving.name}</div>
<div class="d-flex align-items-center justify-content-between">
<span class="d-flex align-items-center gap-2 text-secondary">
<Signal value={priorities[moving.priority]?.value ?? 0} color={priorities[moving.priority]?.color} />
{moving.priority}
</span>
<Avatar person={personById(moving.assigned_to)} size="xs" />
</div>
</div>
<span class="cursor" style="top: 55%; left: 55%"></span>
</div>
</CardBody>
</Card>
</ScreenshotLayout>