Design
llms.txt digitalcrew.tech

Pipeline

MovableCardGrid

A card grid the reader can rearrange.

MaxPipelineDigitalCrew.MovableCardGrid
MovableCardGrid · liveOpen

Guidelines#

In arrange mode each card gets a dashed scrim to drag and a handle with arrow buttons that move it one cell at a time.

Use it for the prospect detail page and the dashboard. Pair it with an "Arrange cards" / "Done" toggle.

What you provide:

  • cards: { key, title, className?, node, actions? }[]. className sets column spans; actions adds controls to the handle after the arrows.
  • arranging: boolean.
  • onMove(key, targetKey, edge: "before" | "after"): the component doesn't reorder anything itself.
  • className: the grid, for example grid gap-4 sm:grid-cols-2 xl:grid-cols-3.

Behaviour:

  • It creates its own DndContext with a PointerSensor that starts after 6px of movement.
  • Arrow moves use the rendered layout. Left and right stay within the row and do nothing when the card is alone in it. Up and down go to the nearest row. A card that spans a row jumps over it.
  • There is no keyboard sensor: the arrow buttons are the accessible way to move cards.

Anatomy:

  • In arrange mode cells are at least min-h-28.
  • The scrim is rounded-xl border-2 border-dashed border-primary/40 bg-background/60.
  • The handle is centred 8px from the top, rounded-md border bg-background px-1.5 py-0.5 shadow-sm, with a GripVertical icon, the title in text-xs font-medium, and 24px ghost icon buttons.
  • A drop target gets ring-2 ring-primary.

Don't wrap it in a DndContext: it creates its own.

Facts#

GroupPipeline
ProductMax
Globalwindow.DigitalCrew.MovableCardGrid
Sourcemax-agent: src/features/prospects/ui/components/movable-card-grid.tsx
Import in the appimport { MovableCardGrid } from "@/features/prospects/ui/components/movable-card-grid";

API#

TypeScript declarations emitted from src/features/prospects/ui/components/movable-card-grid.tsx.

export type MoveEdge = "before" | "after";
export type MoveDirection = "left" | "right" | "up" | "down";
export interface MovableGridCard {
    key: string;
    /** Shown on the arrange-mode handle so an empty card is still nameable. */
    title: string;
    /** Extra classes for the grid cell (column spans). */
    className?: string;
    node: ReactNode;
    /** Extra per-card controls on the arrange-mode handle, after the arrows. */
    actions?: ReactNode;
}
export declare function MovableCardGrid({ cards, arranging, onMove, className, }: {
    cards: MovableGridCard[];
    arranging: boolean;
    onMove: (key: string, targetKey: string, edge: MoveEdge) => void;
    className?: string;
}): JSX.Element;

Example#

The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.

<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  function tile(title, Icon, lines) {
    return h(DC.Card, { className: "gap-2 py-4" },
      h(DC.CardHeader, { className: "px-4" },
        h(DC.CardTitle, { className: "flex items-center gap-2 text-sm" }, h(Icon, { className: "size-4 text-muted-foreground" }), title)),
      h(DC.CardContent, { className: "space-y-1 px-4 text-sm" },
        lines.map(function (l) {
          return h("div", { key: l[0], className: "flex justify-between gap-3" },
            h("span", { className: "text-muted-foreground" }, l[0]), h("span", { className: "truncate font-medium" }, l[1]));
        })));
  }
  var INITIAL = [
    { key: "contact", title: "Contact", node: tile("Contact", I.User, [["Name", "Claire Dubois"], ["Role", "CFO"], ["Email", "claire.dubois@northwind.com"]]) },
    { key: "company", title: "Company", node: tile("Company", I.Building2, [["Name", "Northwind Traders"], ["Size", "480 employees"], ["HQ", "Lyon, France"]]) },
    { key: "engagement", title: "Engagement", node: tile("Engagement", I.TrendingUp, [["Emails opened", "4 of 5"], ["Replies", "1"], ["LinkedIn", "Connected"]]) },
    { key: "next", title: "Next step", node: tile("Next step", I.Calendar, [["Meeting", "Thu 1 Oct, 10:00"], ["Owner", "Sarah Lindqvist"], ["Stage", "Interested"]]) }
  ];
  function move(cards, key, targetKey, edge) {
    var moving = cards.filter(function (c) { return c.key === key; })[0];
    var rest = cards.filter(function (c) { return c.key !== key; });
    var at = rest.findIndex(function (c) { return c.key === targetKey; });
    if (!moving || at === -1) return cards;
    rest.splice(edge === "after" ? at + 1 : at, 0, moving);
    return rest;
  }
  function App() {
    var s = React.useState(INITIAL), cards = s[0], setCards = s[1];
    var a = React.useState(true), arranging = a[0], setArranging = a[1];
    return h("div", { className: "space-y-4" },
      h("div", { className: "flex items-center justify-between" },
        h("p", { className: "text-sm font-semibold" }, "Claire Dubois"),
        h(DC.Button, { variant: arranging ? "default" : "outline", size: "sm", className: "gap-1.5", "aria-pressed": arranging,
          onClick: function () { setArranging(!arranging); } },
          h(I.LayoutDashboard, { className: "h-3.5 w-3.5" }), arranging ? "Done" : "Arrange cards")),
      h(DC.MovableCardGrid, {
        cards: cards, arranging: arranging, className: "grid gap-4 sm:grid-cols-2",
        onMove: function (key, targetKey, edge) { setCards(function (prev) { return move(prev, key, targetKey, edge); }); }
      }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>