Agent Skillsopenchamber/openchamber › drag-to-reorder

drag-to-reorder

GitHub

指导在 OpenChamber 中实现 @dnd-kit 拖拽排序,解决多行可变宽度布局、移动端触摸交互及桌面端兼容问题,避免拉伸和重叠等常见陷阱。

.agents/skills/drag-to-reorder/SKILL.md openchamber/openchamber

Trigger Scenarios

需要添加可重排序列表或网格 修复拖拽项拉伸、重叠或移动端触摸失效的排序组件

Install

npx skills add openchamber/openchamber --skill drag-to-reorder -g -y
More Options

Non-standard path

npx skills add https://github.com/openchamber/openchamber/tree/main/.agents/skills/drag-to-reorder -g -y

Use without installing

npx skills use openchamber/openchamber@drag-to-reorder

指定 Agent (Claude Code)

npx skills add openchamber/openchamber --skill drag-to-reorder -a claude-code -g -y

安装 repo 全部 skill

npx skills add openchamber/openchamber --all -g -y

预览 repo 内 skill

npx skills add openchamber/openchamber --list

SKILL.md

Frontmatter
{
    "name": "drag-to-reorder",
    "license": "MIT",
    "description": "Use when implementing or modifying OpenChamber sortable or drag-to-reorder behavior, especially `@dnd-kit`, touch\/mobile interactions, variable-width items, or wrapping layouts.",
    "compatibility": "opencode"
}

Overview

OpenChamber uses @dnd-kit (@dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities) for drag-to-reorder. Getting it to feel right on both desktop and mobile — especially for chips/tags of variable width that wrap onto multiple rows — has several non-obvious gotchas. This skill captures the configuration that works, and the bugs to avoid.

Reference implementations in this repo:

  • packages/ui/src/components/chat/DraftPresetChips.tsx — variable-width chips that wrap (the hardest case; mobile long-press).
  • packages/ui/src/components/ui/sortable-tabs-strip.tsx — single-row scrollable tabs.

When to Use

  • Adding any reorderable list, row of chips/tags, or sortable grid.
  • Fixing an existing sortable that stretches the dragged item, jumps/overlaps across rows, throws "Maximum update depth exceeded", or doesn't work on touch.

The Five Rules (most important first)

1. Translate, not Transform — kills the "stretch to slot width" bug

CSS.Transform.toString(transform) emits translate3d(...) scaleX(..) scaleY(..). For variable-width items the sorting strategy puts a non-1 scale on the lifted item so it stretches to the neighbor/slot width. Use CSS.Translate.toString(transform) — translation only, no scale — and the dragged item keeps its own size. Sibling shifting is unaffected (their scale was 1 anyway).

import { CSS } from '@dnd-kit/utilities';
const { transform, transition, isDragging } = useSortable({ id });
<div style={{ transform: CSS.Translate.toString(transform), transition }} />
//                  ^^^^^^^^^ NOT CSS.Transform.toString

2. Pick the strategy by layout

Layout Strategy
Wraps onto multiple rows / grid / chips of variable width rectSortingStrategy (default — computes 2D positions, so items can shift to other rows)
Guaranteed single horizontal row (e.g. scrollable tab strip) horizontalListSortingStrategy
Single vertical list verticalListSortingStrategy

horizontalListSortingStrategy on a wrapping row is the classic mistake: it assumes one row, so dragging to another row makes items overlap instead of reflowing. Use rectSortingStrategy for anything that wraps.

3. Desktop + mobile = two sensors (MouseSensor + TouchSensor with delay)

Do NOT use a single PointerSensor — a distance constraint makes touch fight scrolling, and a delay constraint would force desktop to hold-before-drag. Split them:

import { MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';

const sensors = useSensors(
  // Desktop: drag after a small move; a click still fires.
  useSensor(MouseSensor, { activationConstraint: { distance: 8 } }),
  // Touch: long-press to drag — tap fires the element's onClick, a quick swipe scrolls.
  useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 6 } }),
);

This gives: tap = activate the item (e.g. submit), long-press ≈200ms = drag, swipe = scroll.

4. touch-action: none on the draggable

Without it the browser hijacks the touch gesture (scrolls) instead of letting dnd-kit drag. Add Tailwind touch-none (and usually select-none) to the draggable element.

5. Reorder on onDragEnd, with stable ids and arrayMove

import { arrayMove } from '@dnd-kit/sortable';
const onDragEnd = (e: DragEndEvent) => {
  const { active, over } = e;
  if (over && active.id !== over.id) {
    // find by id, never by array index
    const from = items.findIndex(i => i.id === active.id);
    const to = items.findIndex(i => i.id === over.id);
    setItems(arrayMove(items, from, to));
  }
};

IDs must be stable per item (derive from the item's identity, e.g. type:name), never the array index — index ids break tracking after the first move.

Minimal Wiring

import { DndContext, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
import { SortableContext, useSortable, rectSortingStrategy, arrayMove } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

const Item: React.FC<{ id: string; label: string; onClick: () => void }> = ({ id, label, onClick }) => {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
  return (
    <div ref={setNodeRef} style={{ transform: CSS.Translate.toString(transform), transition }} className={cn(isDragging && 'opacity-60')}>
      <button type="button" {...attributes} {...listeners} onClick={onClick} className="touch-none select-none ...">
        {label}
      </button>
    </div>
  );
};

// Configure sensors per Rule 3, reorder onDragEnd per Rule 5, and choose the
// SortableContext strategy from Rule 2. This item wiring preserves item width.

A clickable element can be draggable at the same time: keep onClick on the button and the activation constraint (distance/delay) lets a plain click/tap through.

Symptom Index

Symptom Cause Fix
Dragged item stretches to the target slot width Scale from CSS.Transform.toString Rule 1
On narrow/multi-row: items don't reflow to other rows, overlap, unclear drop target Single-row strategy on wrapping layout Rule 2
"Maximum update depth exceeded" during drag + dragged element floats offset from the cursor Live-reorder in onDragOver (empty strategy + setState each over) oscillates A↔B with variable sizes; the empty DragOverlay we paired with it was mispositioned Don't reorder in onDragOver. Reorder once in onDragEnd (Rule 5). Only reach for live-reorder if you truly need physical row-reflow, and then guard against oscillation.
Touch drag scrolls the page instead of dragging Missing touch ownership Rule 4
Touch: every finger move drags, or tap doesn't register One sensor for mouse and touch Rule 3

If rectSortingStrategy still isn't crisp enough

Reordering variable-width chips across wrapped rows is a documented rough edge in dnd-kit's box strategies. rectSortingStrategy is the best the strategy-based approach offers without instability. If a design needs bulletproof cross-row feedback, switch UX: render a drop-position indicator (a line/gap showing where it will land) and keep items static during the drag (no reorder until drop) — this avoids both overlap and the oscillation loop, at the cost of more code. Discuss before building it.

Key Files

  • Variable-width wrapping chips: packages/ui/src/components/chat/DraftPresetChips.tsx
  • Single-row tab strip: packages/ui/src/components/ui/sortable-tabs-strip.tsx
  • Library: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities (already in packages/ui/package.json)

Completion Criteria

Verify every applicable rule on desktop and touch. Wrapping layouts must preserve item width, reflow across rows, allow taps and scrolling before long-press activation, and reorder exactly once on drag end with stable IDs.

Version History

  • 2db90f7 Current 2026-08-20 04:55

    文档更新:优化了 Agent Skill 的指导说明。

  • 74b1bd8 2026-07-25 10:36

Same Skill Collection

.agents/skills/changelog-authoring/SKILL.md
.agents/skills/clack-cli-patterns/SKILL.md
.agents/skills/desktop-shell/SKILL.md
.agents/skills/locale-ui-patterns/SKILL.md
.agents/skills/openchamber-change-discipline/SKILL.md
.agents/skills/performance-engineering/SKILL.md
.agents/skills/relay-transport/SKILL.md
.agents/skills/serve-sim/SKILL.md
.agents/skills/settings-ui-patterns/SKILL.md
.agents/skills/sync-state-invariants/SKILL.md
.agents/skills/theme-system/SKILL.md
.agents/skills/ui-api-decoupling/SKILL.md
.agents/skills/writing-for-agents/SKILL.md

Metadata

Files
0
Version
2db90f7
Hash
2f2cb493
Indexed
2026-07-25 10:36

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-20 10:08
浙ICP备14020137号-1 $Гость$