feat: smooth sortable row interactions

This commit is contained in:
3252a8
2026-06-03 12:40:57 +03:00
parent 0a5575c1e6
commit 7b8feee99a
5 changed files with 135 additions and 8 deletions
@@ -353,6 +353,8 @@
}
.theme-key-ascii .ui-sortable {
--sortable-drop-line: #ffffff;
--sortable-drop-soft: rgba(255, 255, 255, 0.08);
gap: 6px;
}
@@ -364,6 +366,15 @@
outline: 1px dashed #ffffff;
outline-offset: 2px;
background: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
.theme-key-ascii .ui-sortable-item.is-drop-target::before {
top: -5px;
height: 1px;
border-radius: 0;
background: #ffffff;
box-shadow: none;
}
.theme-key-ascii .ui-sortable-handle {
+12 -1
View File
@@ -236,9 +236,20 @@ body:has(.theme-key-light) .install-platform-item[data-selected] {
color: color-mix(in srgb, var(--accent) 58%, #0f172a);
}
.theme-key-light .ui-sortable {
--sortable-drop-line: color-mix(in srgb, var(--accent) 64%, #0f172a);
}
.theme-key-light .ui-sortable-item.is-drop-target {
outline-color: color-mix(in srgb, var(--accent) 48%, #0f172a);
background: color-mix(in srgb, var(--accent) 8%, #ffffff);
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--accent) 26%, transparent),
0 10px 22px color-mix(in srgb, var(--accent) 7%, transparent);
}
.theme-key-light .ui-sortable-item.is-drop-target::before {
background: var(--sortable-drop-line);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
/* Telegram notifications banner: keep the warm warning tint but swap the
@@ -1231,6 +1231,16 @@ body:has(.theme-key-windows95) .install-platform-item[data-selected] {
background: color-mix(in srgb, var(--accent) 12%, var(--admin-surface));
}
.theme-key-windows95 .ui-sortable-item.is-drop-target::before {
top: -7px;
height: 2px;
border-radius: 0;
background: #000080;
box-shadow:
0 1px 0 #ffffff,
0 -1px 0 #000000;
}
.theme-key-windows95 .ui-sortable-handle {
align-self: center;
width: 24px;
+101 -7
View File
@@ -1,4 +1,6 @@
<script>
import { flip } from "svelte/animate";
import { cubicOut } from "svelte/easing";
import { cn } from "$lib/utils.js";
import { GripVertical } from "./icons.js";
@@ -19,15 +21,37 @@
let dragIndex = null;
let dropIndex = null;
$: dragActive = dragIndex !== null;
const flipConfig = {
duration(distance) {
if (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
return 0;
}
return Math.min(220, 110 + distance * 0.35);
},
easing: cubicOut,
};
function handleDragStart(event, index) {
if (disabled) return;
if (disabled) {
event.preventDefault();
return;
}
dragIndex = index;
dropIndex = index;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
// Firefox requires data to be set for a drag to start.
event.dataTransfer.setData("text/plain", String(index));
const dragRow = event.currentTarget?.closest?.(".ui-sortable-item");
if (dragRow) {
const rect = dragRow.getBoundingClientRect();
event.dataTransfer.setDragImage(dragRow, 18, rect.height / 2);
}
}
}
@@ -35,7 +59,7 @@
if (dragIndex === null) return;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
dropIndex = index;
if (dropIndex !== index) dropIndex = index;
}
function handleDrop(event, index) {
@@ -52,13 +76,14 @@
}
</script>
<div class={cn("ui-sortable", containerClass)} role="list">
<div class={cn("ui-sortable", containerClass)} class:is-drag-active={dragActive} role="list">
{#each items as item, index (getKey(item, index))}
<div
class={cn("ui-sortable-item", className)}
class:is-dragging={dragIndex === index}
class:is-drop-target={dropIndex === index && dragIndex !== index}
role="listitem"
animate:flip={flipConfig}
on:dragover={(event) => handleDragOver(event, index)}
on:drop={(event) => handleDrop(event, index)}
on:dragend={reset}
@@ -67,7 +92,9 @@
type="button"
class="ui-sortable-handle"
draggable={!disabled}
{disabled}
aria-label={handleLabel}
aria-grabbed={dragIndex === index}
title={handleLabel}
on:dragstart={(event) => handleDragStart(event, index)}
>
@@ -80,19 +107,54 @@
<style>
.ui-sortable {
--sortable-accent: var(--admin-ring, var(--admin-accent, var(--accent, #4f8cff)));
--sortable-drop-soft: color-mix(in srgb, var(--sortable-accent) 10%, transparent);
--sortable-drop-line: color-mix(
in srgb,
var(--sortable-accent) 78%,
var(--admin-text, #ffffff)
);
display: grid;
gap: 8px;
min-width: 0;
}
.ui-sortable-item {
position: relative;
min-width: 0;
border-radius: 8px;
transition:
background-color 160ms ease,
box-shadow 160ms ease,
opacity 160ms ease,
transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ui-sortable-item.is-dragging {
opacity: 0.5;
opacity: 0.46;
transform: scale(0.992);
}
.ui-sortable-item.is-drop-target {
outline: 2px dashed var(--admin-accent, #4f8cff);
outline-offset: 2px;
border-radius: 8px;
background: var(--sortable-drop-soft);
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--sortable-accent) 38%, transparent),
0 8px 24px color-mix(in srgb, var(--sortable-accent) 8%, transparent);
transform: translateY(-1px);
}
.ui-sortable-item.is-drop-target::before {
content: "";
position: absolute;
top: -6px;
left: 0;
right: 0;
z-index: 1;
height: 3px;
border-radius: 999px;
background: var(--sortable-drop-line);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--sortable-accent) 16%, transparent);
pointer-events: none;
}
.ui-sortable-handle {
@@ -103,17 +165,49 @@
height: 100%;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--admin-muted, inherit);
cursor: grab;
touch-action: none;
transition:
background-color 160ms ease,
color 160ms ease,
transform 160ms ease,
box-shadow 160ms ease;
}
.ui-sortable-handle:hover {
background: color-mix(in srgb, var(--sortable-accent) 10%, transparent);
color: var(--admin-text, inherit);
}
.ui-sortable-handle:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--sortable-accent) 22%, transparent);
color: var(--admin-text, inherit);
}
.ui-sortable-handle:active {
cursor: grabbing;
transform: scale(0.94);
}
.ui-sortable-handle:disabled {
cursor: default;
opacity: 0.5;
}
@media (prefers-reduced-motion: reduce) {
.ui-sortable-item,
.ui-sortable-handle {
transition: none;
}
.ui-sortable-item.is-dragging,
.ui-sortable-item.is-drop-target,
.ui-sortable-handle:active {
transform: none;
}
}
</style>
+1
View File
@@ -576,6 +576,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
"ui-sortable-item",
"ui-sortable-handle",
"is-drop-target",
"ui-sortable-item.is-drop-target::before",
)
for key in ("light", "ascii", "windows95"):