Row Dragging
Drag and drop rows within the same LyteNyte Grid, across multiple grids, or into external drop zones and applications.
Single Row Dragging
LyteNyte Grid provides the api.useRowDrag hook to enable row dragging.
This hook returns drag props and handlers you can apply to a drag handle component.
The handle can be any component that supports standard HTML drag-and-drop attributes,
such as draggable and onDragStart.
The demo below demonstrates the basic row dragging setup. Users can drag rows to reorder
them within the same grid. The demo uses LyteNyte Grid’s onRowDragEnter, onRowDragLeave,
and onRowDrop properties to attach callbacks for drag events.
Single Row Dragging
81 collapsed lines
1import "./demo.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import "@1771technologies/lytenyte-pro/pill-manager.css";4import { Grid, moveRelative, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 ExchangeCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data as initialData } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { DragHandleDots2Icon } from "@radix-ui/react-icons";16import { useState } from "react";17
18export interface GridSpec {19 readonly data: DEXPerformanceData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },24 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },25 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },26
27 {28 id: "change24h",29 cellRenderer: PercentCellPositiveNegative,30 headerRenderer: makePerfHeaderCell("Change", "24h"),31 name: "Change % 24h",32 type: "number,",33 },34
35 {36 id: "perf1w",37 cellRenderer: PercentCellPositiveNegative,38 headerRenderer: makePerfHeaderCell("Perf %", "1w"),39 name: "Perf % 1W",40 type: "number,",41 },42 {43 id: "perf1m",44 cellRenderer: PercentCellPositiveNegative,45 headerRenderer: makePerfHeaderCell("Perf %", "1m"),46 name: "Perf % 1M",47 type: "number,",48 },49 {50 id: "perf3m",51 cellRenderer: PercentCellPositiveNegative,52 headerRenderer: makePerfHeaderCell("Perf %", "3m"),53 name: "Perf % 3M",54 type: "number,",55 },56 {57 id: "perf6m",58 cellRenderer: PercentCellPositiveNegative,59 headerRenderer: makePerfHeaderCell("Perf %", "6m"),60 name: "Perf % 6M",61 type: "number,",62 },63 {64 id: "perfYtd",65 cellRenderer: PercentCellPositiveNegative,66 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),67 name: "Perf % YTD",68 type: "number",69 },70 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },71 {72 id: "volatility1m",73 cellRenderer: PercentCell,74 headerRenderer: makePerfHeaderCell("Volatility", "1m"),75 name: "Volatility 1M",76 type: "number",77 },78];79
80const base: Grid.ColumnBase<GridSpec> = { width: 80 };81
82
83const marker: Grid.ColumnMarker<GridSpec> = { on: true, cellRenderer: MarkerCell };84
85export default function RowDemo() {86 const [data, setData] = useState(initialData);87
88 const ds = useClientDataSource({ data });89
90 return (91 <div92 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-cell-marker:px-0 ln-cell-marker:border-e ln-cell-marker:border-ln-border"93 style={{ height: 500 }}94 >95 <Grid96 columns={columns}97 columnBase={base}98 rowSource={ds}99 columnMarker={marker}100 onRowDragEnter={(p) => {101 if (p.over.kind === "viewport") return;102
103 const overIndex = p.over.rowIndex;104 const dragIndex = p.source.rowIndex;105
106 if (overIndex === dragIndex) return;107
108 if (overIndex < dragIndex) p.over.element.setAttribute("data-ln-drag-position", "before");109 else p.over.element.setAttribute("data-ln-drag-position", "after");110 }}111 onRowDragLeave={(p) => {112 if (p.over.kind === "viewport") return;113 p.over.element.removeAttribute("data-ln-drag-position");114 }}115 onRowDrop={(p) => {116 if (p.over.kind === "viewport") return;117 p.over.element.removeAttribute("data-ln-drag-position");118
119 setData((prev) => {120 if (p.over.kind === "viewport") return prev;121
122 const next = moveRelative(prev, p.source.rowIndex, p.over.rowIndex);123
124 return next;125 });126 }}127 />128 </div>129 );130}131
132function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {133 const { props } = api.useRowDrag({ rowIndex });134
135 return (136 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>137 <DragHandleDots2Icon />138 </div>139 );140}1[data-ln-drag-position="before"]::before {2 position: absolute;3 top: 0px;4 inset-inline-start: 0px;5 content: "";6 height: 2px;7 background-color: var(--ln-primary-50);8 width: 100%;9 z-index: 10;10}11
12[data-ln-drag-position="after"]::before {13 position: absolute;14 top: calc(var(--ln-row-height) - 2px);15 inset-inline-start: 0px;16 content: "";17 height: 2px;18 background-color: var(--ln-primary-50);19 width: 100%;20 z-index: 10;21}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11
12export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {13 if (!api.rowIsLeaf(row) || !row.data) return null;14
15 const ticker = row.data.symbolTicker;16 const symbol = row.data.symbol;17 const image = symbols[row.data.symbolTicker];18
19 return (20 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">21 <div>22 <img23 src={image}24 alt={`Logo for symbol ${symbol}`}25 className="h-full w-full overflow-hidden rounded-full"26 />27 </div>28 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">29 {ticker}30 </div>31 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>32 </div>33 );34}35
36export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {37 if (!api.rowIsLeaf(row) || !row.data) return null;38
39 const name = row.data.network;40 const image = networks[name];41
42 return (43 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">44 <div>45 <img46 src={image}47 alt={`Logo for network ${name}`}48 className="h-full w-full overflow-hidden rounded-full"49 />50 </div>51 <div className="w-full overflow-hidden text-ellipsis">{name}</div>52 </div>53 );54}55
56export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {57 if (!api.rowIsLeaf(row) || !row.data) return null;58
59 const name = row.data.exchange;60 const image = exchanges[name];61
62 return (63 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">64 <div>65 <img66 src={image}67 alt={`Logo for exchange ${name}`}68 className="h-full w-full overflow-hidden rounded-full"69 />70 </div>71 <div className="w-full overflow-hidden text-ellipsis">{name}</div>72 </div>73 );74}75
76export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {77 if (!api.rowIsLeaf(row) || !row.data) return null;78
79 const field = api.columnField(column, row);80
81 if (typeof field !== "number") return "-";82
83 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";84
85 return (86 <div87 className={tw(88 "h-ful flex w-full items-center justify-end tabular-nums",89 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",90 )}91 >92 {value}93 </div>94 );95}96
97export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {98 if (!api.rowIsLeaf(row) || !row.data) return null;99
100 const field = api.columnField(column, row);101
102 if (typeof field !== "number") return "-";103
104 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";105
106 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;107}108
109export const makePerfHeaderCell = (name: string, subname: string) => {110 return (_: Grid.T.HeaderParams<GridSpec>) => {111 return (112 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">113 <div>{name}</div>114 <div className="text-ln-text-light font-mono uppercase">{subname}</div>115 </div>116 );117 };118};The api.useRowDrag hook requires a rowIndex. LyteNyte Grid creates the necessary drag data when the
drag operation begins. The code for the drag handle is shown below.
1function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {2 const { props } = api.useRowDrag({ rowIndex });3
4 return (5 <div {...props}>6 <DragHandleDots2Icon />7 </div>8 );9}The onRowDragEnter, onRowDragLeave, and onRowDrop callbacks receive parameters describing the
drag operation. The parameter object has two properties:
source: Data related to the row being dragged.over: Data related to the row or viewport being dragged over.
Using these properties, you can perform actions when events occur, such as adding and removing drag indicators, or reordering rows.
Multi-Row Dragging
Combine row dragging with row selection to support dragging multiple rows.
The drag event parameters include a source property that provides the grid
API for the row being dragged. Use this API to retrieve the selected rows
and move them together with the current row. This is shown in the demo below.
Multiple Row Dragging
81 collapsed lines
1import "./demo.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import "@1771technologies/lytenyte-pro/pill-manager.css";4import { Grid, moveRelative, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 ExchangeCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data as initialData } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { DragHandleDots2Icon } from "@radix-ui/react-icons";16import { useState } from "react";17
18export interface GridSpec {19 readonly data: DEXPerformanceData;20}21
22const columns: Grid.Column<GridSpec>[] = [23 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },24 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },25 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },26
27 {28 id: "change24h",29 cellRenderer: PercentCellPositiveNegative,30 headerRenderer: makePerfHeaderCell("Change", "24h"),31 name: "Change % 24h",32 type: "number,",33 },34
35 {36 id: "perf1w",37 cellRenderer: PercentCellPositiveNegative,38 headerRenderer: makePerfHeaderCell("Perf %", "1w"),39 name: "Perf % 1W",40 type: "number,",41 },42 {43 id: "perf1m",44 cellRenderer: PercentCellPositiveNegative,45 headerRenderer: makePerfHeaderCell("Perf %", "1m"),46 name: "Perf % 1M",47 type: "number,",48 },49 {50 id: "perf3m",51 cellRenderer: PercentCellPositiveNegative,52 headerRenderer: makePerfHeaderCell("Perf %", "3m"),53 name: "Perf % 3M",54 type: "number,",55 },56 {57 id: "perf6m",58 cellRenderer: PercentCellPositiveNegative,59 headerRenderer: makePerfHeaderCell("Perf %", "6m"),60 name: "Perf % 6M",61 type: "number,",62 },63 {64 id: "perfYtd",65 cellRenderer: PercentCellPositiveNegative,66 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),67 name: "Perf % YTD",68 type: "number",69 },70 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },71 {72 id: "volatility1m",73 cellRenderer: PercentCell,74 headerRenderer: makePerfHeaderCell("Volatility", "1m"),75 name: "Volatility 1M",76 type: "number",77 },78];79
80const base: Grid.ColumnBase<GridSpec> = { width: 80 };81
82
83const marker: Grid.ColumnMarker<GridSpec> = { on: true, cellRenderer: MarkerCell };84
85export default function RowDemo() {86 const [data, setData] = useState(initialData);87
88 const ds = useClientDataSource({ data });89
90 return (91 <div92 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-cell-marker:px-0 ln-cell-marker:border-e ln-cell-marker:border-ln-border"93 style={{ height: 500 }}94 >95 <Grid96 columns={columns}97 columnBase={base}98 rowSource={ds}99 columnMarker={marker}100 rowSelectionMode="multiple"101 onRowDragEnter={(p) => {102 if (p.over.kind === "viewport") return;103
104 const overIndex = p.over.rowIndex;105 const dragIndex = p.source.rowIndex;106
107 if (overIndex === dragIndex) return;108
109 if (overIndex < dragIndex) p.over.element.setAttribute("data-ln-drag-position", "before");110 else p.over.element.setAttribute("data-ln-drag-position", "after");111 }}112 onRowDragLeave={(p) => {113 if (p.over.kind === "viewport") return;114 p.over.element.removeAttribute("data-ln-drag-position");115 }}116 onRowDrop={(p) => {117 if (p.over.kind === "viewport") return;118
119 const viewport = p.source.api.viewport();120 // React conciliation may preserve some node when they are re-ordered (especially since we are modifying the elements121 // outside of react.122 setTimeout(() => {123 viewport?.querySelector("[data-ln-drag-position]")?.removeAttribute("data-ln-drag-position");124 });125
126 setData((prev) => {127 if (p.over.kind === "viewport") return prev;128
129 const selected = p.source.api130 .rowsSelected()131 .rows.filter((x) => x.id !== p.source.row.id)132 .map((x) => p.source.api.rowIdToRowIndex(x.id))133 .filter((b) => b != null)134 .sort((l, r) => l - r);135
136 const next = moveRelative(prev, p.source.rowIndex, p.over.rowIndex, selected);137
138 return next;139 });140
141 p.source.api.rowSelect({ selected: "all", deselect: true });142 }}143 />144 </div>145 );146}147
148function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {149 const { props } = api.useRowDrag({ rowIndex });150
151 return (152 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>153 <DragHandleDots2Icon />154 </div>155 );156}1[data-ln-drag-position="before"]::before {2 position: absolute;3 top: 0px;4 inset-inline-start: 0px;5 content: "";6 height: 2px;7 background-color: var(--ln-primary-50);8 width: 100%;9 z-index: 10;10}11
12[data-ln-drag-position="after"]::before {13 position: absolute;14 top: calc(var(--ln-row-height) - 2px);15 inset-inline-start: 0px;16 content: "";17 height: 2px;18 background-color: var(--ln-primary-50);19 width: 100%;20 z-index: 10;21}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11
12export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {13 if (!api.rowIsLeaf(row) || !row.data) return null;14
15 const ticker = row.data.symbolTicker;16 const symbol = row.data.symbol;17 const image = symbols[row.data.symbolTicker];18
19 return (20 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">21 <div>22 <img23 src={image}24 alt={`Logo for symbol ${symbol}`}25 className="h-full w-full overflow-hidden rounded-full"26 />27 </div>28 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">29 {ticker}30 </div>31 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>32 </div>33 );34}35
36export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {37 if (!api.rowIsLeaf(row) || !row.data) return null;38
39 const name = row.data.network;40 const image = networks[name];41
42 return (43 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">44 <div>45 <img46 src={image}47 alt={`Logo for network ${name}`}48 className="h-full w-full overflow-hidden rounded-full"49 />50 </div>51 <div className="w-full overflow-hidden text-ellipsis">{name}</div>52 </div>53 );54}55
56export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {57 if (!api.rowIsLeaf(row) || !row.data) return null;58
59 const name = row.data.exchange;60 const image = exchanges[name];61
62 return (63 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">64 <div>65 <img66 src={image}67 alt={`Logo for exchange ${name}`}68 className="h-full w-full overflow-hidden rounded-full"69 />70 </div>71 <div className="w-full overflow-hidden text-ellipsis">{name}</div>72 </div>73 );74}75
76export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {77 if (!api.rowIsLeaf(row) || !row.data) return null;78
79 const field = api.columnField(column, row);80
81 if (typeof field !== "number") return "-";82
83 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";84
85 return (86 <div87 className={tw(88 "h-ful flex w-full items-center justify-end tabular-nums",89 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",90 )}91 >92 {value}93 </div>94 );95}96
97export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {98 if (!api.rowIsLeaf(row) || !row.data) return null;99
100 const field = api.columnField(column, row);101
102 if (typeof field !== "number") return "-";103
104 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";105
106 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;107}108
109export const makePerfHeaderCell = (name: string, subname: string) => {110 return (_: Grid.T.HeaderParams<GridSpec>) => {111 return (112 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">113 <div>{name}</div>114 <div className="text-ln-text-light font-mono uppercase">{subname}</div>115 </div>116 );117 };118};In the demo code, p.source.api retrieves the selected rows, as shown below. The example ignores drags
over the viewport, since the demo uses dragging to reorder rows.
1setData((prev) => {2 if (p.over.kind === "viewport") return prev;3
4 const selected = p.source.api5 .rowsSelected()6 .rows.filter((x) => x.id !== p.source.row.id)7 .map((x) => p.source.api.rowIdToRowIndex(x.id))8 .filter((b) => b != null)9 .sort((l, r) => l - r);10
11 const next = moveRelative(prev, p.source.rowIndex, p.over.rowIndex, selected);12
13 return next;14});The moveRelative function is a utility provided by LyteNyte Grid. It moves items from one position to
another. It performs a smart swap of rows using the indices provided.
Dragging Between Grids
You can drag rows between grids using the rowDropAccept property. A grid can always drag rows within
itself. To allow dragging between different grids, assign each grid an ID using the gridId property.
Then share those IDs with the grids that should accept drops using the rowDropAccept property.
In the demo below, the first grid is the primary grid and the second grid is the secondary grid. The grid IDs have been shared with each grid so rows can be dragged between them.
Grid-to-Grid Dragging
83 collapsed lines
1import "./demo.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import "@1771technologies/lytenyte-pro/pill-manager.css";4import { Grid, moveRelative, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 DragIcon,7 ExchangeCell,8 makePerfHeaderCell,9 NetworkCell,10 PercentCell,11 PercentCellPositiveNegative,12 SymbolCell,13 tw,14} from "./components.jsx";15import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";16import { data as initialData } from "@1771technologies/grid-sample-data/dex-pairs-performance";17import { DragHandleDots2Icon } from "@radix-ui/react-icons";18import { useMemo, useState } from "react";19
20export interface GridSpec {21 readonly data: DEXPerformanceData;22}23
24const columns: Grid.Column<GridSpec>[] = [25 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },26 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },27 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },28
29 {30 id: "change24h",31 cellRenderer: PercentCellPositiveNegative,32 headerRenderer: makePerfHeaderCell("Change", "24h"),33 name: "Change % 24h",34 type: "number,",35 },36
37 {38 id: "perf1w",39 cellRenderer: PercentCellPositiveNegative,40 headerRenderer: makePerfHeaderCell("Perf %", "1w"),41 name: "Perf % 1W",42 type: "number,",43 },44 {45 id: "perf1m",46 cellRenderer: PercentCellPositiveNegative,47 headerRenderer: makePerfHeaderCell("Perf %", "1m"),48 name: "Perf % 1M",49 type: "number,",50 },51 {52 id: "perf3m",53 cellRenderer: PercentCellPositiveNegative,54 headerRenderer: makePerfHeaderCell("Perf %", "3m"),55 name: "Perf % 3M",56 type: "number,",57 },58 {59 id: "perf6m",60 cellRenderer: PercentCellPositiveNegative,61 headerRenderer: makePerfHeaderCell("Perf %", "6m"),62 name: "Perf % 6M",63 type: "number,",64 },65 {66 id: "perfYtd",67 cellRenderer: PercentCellPositiveNegative,68 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),69 name: "Perf % YTD",70 type: "number",71 },72 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },73 {74 id: "volatility1m",75 cellRenderer: PercentCell,76 headerRenderer: makePerfHeaderCell("Volatility", "1m"),77 name: "Volatility 1M",78 type: "number",79 },80];81
82const base: Grid.ColumnBase<GridSpec> = { width: 80 };83
84
85const marker: Grid.ColumnMarker<GridSpec> = { on: true, cellRenderer: MarkerCell };86
87const leafIdFn: Grid.T.LeafIdFn<GridSpec["data"]> = (d) =>88 `${d.symbolTicker}-${d.exchange}-${d.network}-${d.symbol}`;89
90export default function RowDemo() {91 const [primary, setPrimary] = useState(initialData.slice(2, 20));92 const [secondary, setSecondary] = useState<typeof initialData>(initialData.slice(0, 2));93
94 const ds = useClientDataSource<GridSpec>({ data: primary, leafIdFn });95 const dsOther = useClientDataSource<GridSpec>({ data: secondary, leafIdFn });96
97 const [entered, setEntered] = useState(false);98
99 return (100 <div className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-cell-marker:px-0 ln-cell-marker:border-e ln-cell-marker:border-ln-border flex flex-col gap-8">101 <div style={{ height: 250 }}>102 <Grid103 columns={columns}104 columnBase={base}105 rowSource={ds}106 columnMarker={marker}107 gridId="primary"108 rowDropAccept={useMemo(() => ["secondary"], [])}109 onRowDragEnter={(p) => {110 if (p.over.kind === "viewport") return;111
112 if (p.over.id === p.source.id && p.over.rowIndex === p.source.rowIndex) return;113
114 const isBefore = p.over.id !== p.source.id || p.over.rowIndex > p.source.rowIndex;115
116 if (isBefore) p.over.element.setAttribute("data-ln-drag-position", "after");117 else p.over.element.setAttribute("data-ln-drag-position", "before");118 }}119 onRowDragLeave={(p) => {120 if (p.over.kind === "viewport") return;121 p.over.element.removeAttribute("data-ln-drag-position");122 }}123 onRowDrop={(p) => {124 p.over.element.removeAttribute("data-ln-drag-position");125 if (p.over.id === p.source.id) {126 if (p.over.kind === "viewport") return;127
128 setPrimary((prev) => {129 if (p.over.kind === "viewport") return prev;130 const next = moveRelative(prev, p.source.rowIndex, p.over.rowIndex);131 return next;132 });133 } else {134 // Start by removing the row from the source135 const sourceIndex = secondary.indexOf(p.source.row.data);136
137 setSecondary((prev) => {138 const next = [...prev];139 next.splice(sourceIndex, 1);140 return next;141 });142
143 setPrimary((prev) => {144 if (p.over.kind === "viewport") return [...prev, p.source.row.data];145
146 const next = [...prev];147 next.splice(p.over.rowIndex + 1, 0, p.source.row.data);148 return next;149 });150 }151 }}152 />153 </div>154
155 <div156 style={{ height: 250 }}157 className="border-ln-border ln-cell-marker:border-e ln-cell-marker:border-ln-border border-t"158 >159 <Grid160 gridId="secondary"161 columns={columns}162 columnBase={base}163 rowSource={dsOther}164 columnMarker={marker}165 rowDropAccept={useMemo(() => ["primary"], [])}166 slotRowsOverlay={167 secondary.length ? null : (168 <div className="sticky start-0 top-0 h-0 w-0">169 <div170 className={tw(171 "w-(--ln-vp-width) h-(--ln-vp-row-height) absolute left-0 top-0 flex items-center justify-center",172
173 entered && "bg-ln-primary-10",174 )}175 >176 <div className="flex h-full w-full flex-col items-center justify-center">177 <DragIcon className="size-12" />178 Drag rows from the grid here.179 </div>180 </div>181 </div>182 )183 }184 onRowDragEnter={(p) => {185 if (!secondary.length && p.over.kind === "viewport") {186 setEntered(true);187 return;188 }189 if (p.over.kind === "viewport") return;190
191 if (p.over.id === p.source.id && p.over.rowIndex === p.source.rowIndex) return;192
193 const isBefore = p.over.id !== p.source.id || p.over.rowIndex > p.source.rowIndex;194
195 if (isBefore) p.over.element.setAttribute("data-ln-drag-position", "after");196 else p.over.element.setAttribute("data-ln-drag-position", "before");197 }}198 onRowDragLeave={(p) => {199 setEntered(false);200 if (p.over.kind === "viewport") return;201 p.over.element.removeAttribute("data-ln-drag-position");202 }}203 onRowDrop={(p) => {204 setEntered(false);205 p.over.element.removeAttribute("data-ln-drag-position");206
207 // Moving within my self, so we are just reordering rows within the same view.208 if (p.over.id === p.source.id) {209 if (p.over.kind === "viewport") return;210
211 setSecondary((prev) => {212 if (p.over.kind === "viewport") return prev;213 const next = moveRelative(prev, p.source.rowIndex, p.over.rowIndex);214 return next;215 });216 } else {217 // Start by removing the row from the source218 const sourceIndex = primary.indexOf(p.source.row.data);219
220 console.log(sourceIndex);221
222 setPrimary((prev) => {223 const next = [...prev];224 next.splice(sourceIndex, 1);225 return next;226 });227
228 setSecondary((prev) => {229 if (p.over.kind === "viewport") return [...prev, p.source.row.data];230
231 const next = [...prev];232 next.splice(p.over.rowIndex + 1, 0, p.source.row.data);233 return next;234 });235 }236 }}237 />238 </div>239 </div>240 );241}242
243function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {244 const { props } = api.useRowDrag({ rowIndex });245
246 return (247 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>248 <DragHandleDots2Icon />249 </div>250 );251}1[data-ln-drag-position="before"]::before {2 position: absolute;3 top: 0px;4 inset-inline-start: 0px;5 content: "";6 height: 2px;7 background-color: var(--ln-primary-50);8 width: 100%;9 z-index: 10;10}11
12[data-ln-drag-position="after"]::before {13 position: absolute;14 top: calc(var(--ln-row-height) - 2px);15 inset-inline-start: 0px;16 content: "";17 height: 2px;18 background-color: var(--ln-primary-50);19 width: 100%;20 z-index: 10;21}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import type { SVGProps } from "react";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export const DragIcon = (props: SVGProps<SVGSVGElement>) => (122 <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="none" viewBox="0 0 20 21" {...props}>123 <path124 stroke="#676D80"125 strokeDasharray="0.11 2.63"126 strokeLinecap="square"127 strokeLinejoin="round"128 strokeWidth={1.578}129 d="M12.526 3.71v0c0-1.04-.843-1.883-1.883-1.883H4.11a2.104 2.104 0 0 0-2.104 2.104v6.422c0 1.1.892 1.993 1.993 1.993v0"130 />131 <path132 stroke="#676D80"133 strokeLinecap="round"134 strokeLinejoin="round"135 strokeWidth={1.578}136 d="M13.183 17.081c-.39.831-1.596.758-1.884-.113l-1.868-5.652a1.016 1.016 0 0 1 1.283-1.284l5.652 1.869c.872.288.944 1.494.113 1.884l-1.91.899c-.214.1-.386.273-.487.487z"137 />138 <path139 stroke="#676D80"140 strokeLinecap="round"141 strokeLinejoin="round"142 strokeWidth={1.578}143 d="M15.519 8.3v-.986a2.104 2.104 0 0 0-2.104-2.104H7.104A2.104 2.104 0 0 0 5 7.314v6.31c0 1.163.942 2.105 2.104 2.105h.377"144 />145 </svg>146);You can identify whether the drag operation comes from the same grid or another grid using the id property
on the source and over parameters, as shown below.
1onRowDrop={(p) => {2 if (p.over.id === p.source.id) {3 // Same grid4 } else {5 // Different grids6 }7}}Both the source and over properties include a reference to the api of their respective grids. This is
useful when you need to retrieve information or perform actions across different grids.
Note
LyteNyte Grid cannot fully type the api property on the source and over parameters, because
drag operations can come from any grid. Use a type assertion when you need access to API methods specific to
your grid.
1const api = source.api as Grid.API<MySpec>;External Drop Zones
LyteNyte Grid’s row dragging leverages the native browser
drag and drop API.
This means you can implement external drop zones by handling onDrop on an element. The demo below
demonstrates this in action.
External Drop Zones
84 collapsed lines
1import "./demo.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import "@1771technologies/lytenyte-pro/pill-manager.css";4import { getRowDragData, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 DragIcon,7 ExchangeCell,8 makePerfHeaderCell,9 NetworkCell,10 PercentCell,11 PercentCellPositiveNegative,12 SymbolCell,13 SymbolLabel,14 tw,15} from "./components.jsx";16import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";17import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";18import { DragHandleDots2Icon } from "@radix-ui/react-icons";19import { useMemo, useState } from "react";20
21export interface GridSpec {22 readonly data: DEXPerformanceData;23}24
25const columns: Grid.Column<GridSpec>[] = [26 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },27 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },28 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },29
30 {31 id: "change24h",32 cellRenderer: PercentCellPositiveNegative,33 headerRenderer: makePerfHeaderCell("Change", "24h"),34 name: "Change % 24h",35 type: "number,",36 },37
38 {39 id: "perf1w",40 cellRenderer: PercentCellPositiveNegative,41 headerRenderer: makePerfHeaderCell("Perf %", "1w"),42 name: "Perf % 1W",43 type: "number,",44 },45 {46 id: "perf1m",47 cellRenderer: PercentCellPositiveNegative,48 headerRenderer: makePerfHeaderCell("Perf %", "1m"),49 name: "Perf % 1M",50 type: "number,",51 },52 {53 id: "perf3m",54 cellRenderer: PercentCellPositiveNegative,55 headerRenderer: makePerfHeaderCell("Perf %", "3m"),56 name: "Perf % 3M",57 type: "number,",58 },59 {60 id: "perf6m",61 cellRenderer: PercentCellPositiveNegative,62 headerRenderer: makePerfHeaderCell("Perf %", "6m"),63 name: "Perf % 6M",64 type: "number,",65 },66 {67 id: "perfYtd",68 cellRenderer: PercentCellPositiveNegative,69 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),70 name: "Perf % YTD",71 type: "number",72 },73 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },74 {75 id: "volatility1m",76 cellRenderer: PercentCell,77 headerRenderer: makePerfHeaderCell("Volatility", "1m"),78 name: "Volatility 1M",79 type: "number",80 },81];82
83const base: Grid.ColumnBase<GridSpec> = { width: 80 };84
85
86const marker: Grid.ColumnMarker<GridSpec> = { on: true, cellRenderer: MarkerCell };87
88const leafIdFn: Grid.T.LeafIdFn<GridSpec["data"]> = (d) =>89 `${d.symbolTicker}-${d.exchange}-${d.network}-${d.symbol}`;90
91export default function RowDemo() {92 const ds = useClientDataSource<GridSpec>({ data: data, leafIdFn });93
94 const [over, setOver] = useState(false);95
96 const [external, setData] = useState<GridSpec["data"][]>([]);97
98 return (99 <div className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-cell-marker:px-0 ln-cell-marker:border-e ln-cell-marker:border-ln-border flex flex-col gap-8">100 <div style={{ height: 250 }}>101 <Grid102 columns={columns}103 columnBase={base}104 rowSource={ds}105 columnMarker={marker}106 gridId="primary"107 rowDropAccept={useMemo(() => ["secondary"], [])}108 />109 </div>110
111 <div112 style={{ height: 250 }}113 className={tw("border-ln-border overflow-auto border-t", over && "bg-ln-primary-10")}114 onDragLeave={() => setOver(false)}115 onDragOver={() => setOver(true)}116 onDrop={() => {117 setOver(false);118 const row = getRowDragData();119
120 setData((prev) => [...prev, row.row.data]);121 }}122 >123 {!external.length && (124 <div className="flex h-full w-full flex-col items-center justify-center">125 <DragIcon className="size-12" />126 Drag rows from the grid here.127 </div>128 )}129 {external.length > 0 && (130 <div className="flex flex-wrap gap-2 px-2 py-2 font-mono">131 {external.map((x, i) => {132 return (133 <div className="bg-ln-bg-strong rounded px-2 py-2" key={i}>134 <SymbolLabel data={x} />135 </div>136 );137 })}138 </div>139 )}140 </div>141 </div>142 );143}144
145function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {146 const { props } = api.useRowDrag({ rowIndex });147
148 return (149 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>150 <DragHandleDots2Icon />151 </div>152 );153}1[data-ln-drag-position="before"]::before {2 position: absolute;3 top: 0px;4 inset-inline-start: 0px;5 content: "";6 height: 2px;7 background-color: var(--ln-primary-50);8 width: 100%;9 z-index: 10;10}11
12[data-ln-drag-position="after"]::before {13 position: absolute;14 top: calc(var(--ln-row-height) - 2px);15 inset-inline-start: 0px;16 content: "";17 height: 2px;18 background-color: var(--ln-primary-50);19 width: 100%;20 z-index: 10;21}1import type { SVGProps } from "react";2import type { ClassValue } from "clsx";3import clsx from "clsx";4import { twMerge } from "tailwind-merge";5import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";6
7export function tw(...c: ClassValue[]) {8 return twMerge(clsx(...c));9}10import type { Grid } from "@1771technologies/lytenyte-pro";11import type { GridSpec } from "./demo";12
13export function SymbolLabel({ data }: { data: GridSpec["data"] }) {14 const ticker = data.symbolTicker;15 const symbol = data.symbol;16 const image = symbols[data.symbolTicker];17
18 return (19 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">20 <div>21 <img22 src={image}23 alt={`Logo for symbol ${symbol}`}24 className="h-full w-full overflow-hidden rounded-full"25 />26 </div>27 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">28 {ticker}29 </div>30 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>31 </div>32 );33}34
35export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {36 if (!api.rowIsLeaf(row) || !row.data) return null;37
38 return <SymbolLabel data={row.data} />;39}40
41export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {42 if (!api.rowIsLeaf(row) || !row.data) return null;43
44 const name = row.data.network;45 const image = networks[name];46
47 return (48 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">49 <div>50 <img51 src={image}52 alt={`Logo for network ${name}`}53 className="h-full w-full overflow-hidden rounded-full"54 />55 </div>56 <div className="w-full overflow-hidden text-ellipsis">{name}</div>57 </div>58 );59}60
61export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {62 if (!api.rowIsLeaf(row) || !row.data) return null;63
64 const name = row.data.exchange;65 const image = exchanges[name];66
67 return (68 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">69 <div>70 <img71 src={image}72 alt={`Logo for exchange ${name}`}73 className="h-full w-full overflow-hidden rounded-full"74 />75 </div>76 <div className="w-full overflow-hidden text-ellipsis">{name}</div>77 </div>78 );79}80
81export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {82 if (!api.rowIsLeaf(row) || !row.data) return null;83
84 const field = api.columnField(column, row);85
86 if (typeof field !== "number") return "-";87
88 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";89
90 return (91 <div92 className={tw(93 "h-ful flex w-full items-center justify-end tabular-nums",94 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",95 )}96 >97 {value}98 </div>99 );100}101
102export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {103 if (!api.rowIsLeaf(row) || !row.data) return null;104
105 const field = api.columnField(column, row);106
107 if (typeof field !== "number") return "-";108
109 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";110
111 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;112}113
114export const makePerfHeaderCell = (name: string, subname: string) => {115 return (_: Grid.T.HeaderParams<GridSpec>) => {116 return (117 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">118 <div>{name}</div>119 <div className="text-ln-text-light font-mono uppercase">{subname}</div>120 </div>121 );122 };123};124
125export const DragIcon = (props: SVGProps<SVGSVGElement>) => (126 <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="none" viewBox="0 0 20 21" {...props}>127 <path128 stroke="#676D80"129 strokeDasharray="0.11 2.63"130 strokeLinecap="square"131 strokeLinejoin="round"132 strokeWidth={1.578}133 d="M12.526 3.71v0c0-1.04-.843-1.883-1.883-1.883H4.11a2.104 2.104 0 0 0-2.104 2.104v6.422c0 1.1.892 1.993 1.993 1.993v0"134 />135 <path136 stroke="#676D80"137 strokeLinecap="round"138 strokeLinejoin="round"139 strokeWidth={1.578}140 d="M13.183 17.081c-.39.831-1.596.758-1.884-.113l-1.868-5.652a1.016 1.016 0 0 1 1.283-1.284l5.652 1.869c.872.288.944 1.494.113 1.884l-1.91.899c-.214.1-.386.273-.487.487z"141 />142 <path143 stroke="#676D80"144 strokeLinecap="round"145 strokeLinejoin="round"146 strokeWidth={1.578}147 d="M15.519 8.3v-.986a2.104 2.104 0 0 0-2.104-2.104H7.104A2.104 2.104 0 0 0 5 7.314v6.31c0 1.163.942 2.105 2.104 2.105h.377"148 />149 </svg>150);The getRowDragData function is a helper provided by LyteNyte Grid.
This function returns the current row drag data while a row drag is active.
Use this function to retrieve the drag data for your own components.
Do not call this function unless a row drag operation is in progress.
Row Drag Placeholders
The api.useRowDrag hook accepts an optional placeholder property. Use this property to provide a
custom placeholder for drag operations. The placeholder can be one of the following:
- String: A CSS selector targeting the HTML element to serve as the placeholder. The browser captures a snapshot of this element to generate the placeholder image.
- Query Object: An object containing a query string and an optional offset tuple. This behaves like a string value but allows you to shift the placeholder image using the provided tuple.
- Component Placeholder: A custom React component that renders the placeholder. This option disables browser drag-image snapshots. Use this option only when drag operations stay within the same page.
The demo below demonstrates a React placeholder in action. Notice that the placeholder component uses a
portal to render into the body element.
Drag Placeholder
88 collapsed lines
1import "./demo.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import "@1771technologies/lytenyte-pro/pill-manager.css";4import {5 getRowDragData,6 Grid,7 moveRelative,8 useClientDataSource,9} from "@1771technologies/lytenyte-pro";10import {11 ExchangeCell,12 makePerfHeaderCell,13 NetworkCell,14 PercentCell,15 PercentCellPositiveNegative,16 SymbolCell,17 SymbolLabel,18} from "./components.jsx";19import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";20import { data as initialData } from "@1771technologies/grid-sample-data/dex-pairs-performance";21import { DragHandleDots2Icon } from "@radix-ui/react-icons";22import { useState } from "react";23import { createPortal } from "react-dom";24
25export interface GridSpec {26 readonly data: DEXPerformanceData;27}28
29const columns: Grid.Column<GridSpec>[] = [30 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },31 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },32 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },33
34 {35 id: "change24h",36 cellRenderer: PercentCellPositiveNegative,37 headerRenderer: makePerfHeaderCell("Change", "24h"),38 name: "Change % 24h",39 type: "number,",40 },41
42 {43 id: "perf1w",44 cellRenderer: PercentCellPositiveNegative,45 headerRenderer: makePerfHeaderCell("Perf %", "1w"),46 name: "Perf % 1W",47 type: "number,",48 },49 {50 id: "perf1m",51 cellRenderer: PercentCellPositiveNegative,52 headerRenderer: makePerfHeaderCell("Perf %", "1m"),53 name: "Perf % 1M",54 type: "number,",55 },56 {57 id: "perf3m",58 cellRenderer: PercentCellPositiveNegative,59 headerRenderer: makePerfHeaderCell("Perf %", "3m"),60 name: "Perf % 3M",61 type: "number,",62 },63 {64 id: "perf6m",65 cellRenderer: PercentCellPositiveNegative,66 headerRenderer: makePerfHeaderCell("Perf %", "6m"),67 name: "Perf % 6M",68 type: "number,",69 },70 {71 id: "perfYtd",72 cellRenderer: PercentCellPositiveNegative,73 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),74 name: "Perf % YTD",75 type: "number",76 },77 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },78 {79 id: "volatility1m",80 cellRenderer: PercentCell,81 headerRenderer: makePerfHeaderCell("Volatility", "1m"),82 name: "Volatility 1M",83 type: "number",84 },85];86
87const base: Grid.ColumnBase<GridSpec> = { width: 80 };88
89
90const marker: Grid.ColumnMarker<GridSpec> = { on: true, cellRenderer: MarkerCell };91
92export default function RowDemo() {93 const [data, setData] = useState(initialData);94
95 const ds = useClientDataSource({ data });96
97 return (98 <div99 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-cell-marker:px-0 ln-cell-marker:border-e ln-cell-marker:border-ln-border"100 style={{ height: 500 }}101 >102 <Grid103 columns={columns}104 columnBase={base}105 rowSource={ds}106 columnMarker={marker}107 onRowDragEnter={(p) => {108 if (p.over.kind === "viewport") return;109
110 const overIndex = p.over.rowIndex;111 const dragIndex = p.source.rowIndex;112
113 if (overIndex === dragIndex) return;114
115 if (overIndex < dragIndex) p.over.element.setAttribute("data-ln-drag-position", "before");116 else p.over.element.setAttribute("data-ln-drag-position", "after");117 }}118 onRowDragLeave={(p) => {119 if (p.over.kind === "viewport") return;120 p.over.element.removeAttribute("data-ln-drag-position");121 }}122 onRowDrop={(p) => {123 if (p.over.kind === "viewport") return;124 p.over.element.removeAttribute("data-ln-drag-position");125
126 setData((prev) => {127 if (p.over.kind === "viewport") return prev;128
129 const next = moveRelative(prev, p.source.rowIndex, p.over.rowIndex);130
131 return next;132 });133 }}134 />135 </div>136 );137}138
139function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {140 const { props, placeholder } = api.useRowDrag({ rowIndex, placeholder: Placeholder });141
142 return (143 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>144 <DragHandleDots2Icon />145 {placeholder}146 </div>147 );148}149
150const Placeholder: Grid.T.RowDragPlaceholderFn = ({ x, y }) => {151 const data = getRowDragData();152
153 if (data.row.kind !== "leaf") return;154
155 return createPortal(156 <div157 className="bg-ln-bg-strong border-ln-primary-50 pointer-events-none rounded border px-2 py-2"158 style={{ position: "fixed", top: 0, left: 0, transform: `translate3d(${x}px, ${y}px, 0px)` }}159 >160 <SymbolLabel data={data.row.data} />161 </div>,162 document.body,163 );164};1[data-ln-drag-position="before"]::before {2 position: absolute;3 top: 0px;4 inset-inline-start: 0px;5 content: "";6 height: 2px;7 background-color: var(--ln-primary-50);8 width: 100%;9 z-index: 10;10}11
12[data-ln-drag-position="after"]::before {13 position: absolute;14 top: calc(var(--ln-row-height) - 2px);15 inset-inline-start: 0px;16 content: "";17 height: 2px;18 background-color: var(--ln-primary-50);19 width: 100%;20 z-index: 10;21}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11
12export function SymbolLabel({ data }: { data: GridSpec["data"] }) {13 const ticker = data.symbolTicker;14 const symbol = data.symbol;15 const image = symbols[data.symbolTicker];16
17 return (18 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">19 <div>20 <img21 src={image}22 alt={`Logo for symbol ${symbol}`}23 className="h-full w-full overflow-hidden rounded-full"24 />25 </div>26 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">27 {ticker}28 </div>29 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>30 </div>31 );32}33
34export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {35 if (!api.rowIsLeaf(row) || !row.data) return null;36
37 return <SymbolLabel data={row.data} />;38}39
40export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {41 if (!api.rowIsLeaf(row) || !row.data) return null;42
43 const name = row.data.network;44 const image = networks[name];45
46 return (47 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">48 <div>49 <img50 src={image}51 alt={`Logo for network ${name}`}52 className="h-full w-full overflow-hidden rounded-full"53 />54 </div>55 <div className="w-full overflow-hidden text-ellipsis">{name}</div>56 </div>57 );58}59
60export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {61 if (!api.rowIsLeaf(row) || !row.data) return null;62
63 const name = row.data.exchange;64 const image = exchanges[name];65
66 return (67 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">68 <div>69 <img70 src={image}71 alt={`Logo for exchange ${name}`}72 className="h-full w-full overflow-hidden rounded-full"73 />74 </div>75 <div className="w-full overflow-hidden text-ellipsis">{name}</div>76 </div>77 );78}79
80export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {81 if (!api.rowIsLeaf(row) || !row.data) return null;82
83 const field = api.columnField(column, row);84
85 if (typeof field !== "number") return "-";86
87 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";88
89 return (90 <div91 className={tw(92 "h-ful flex w-full items-center justify-end tabular-nums",93 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",94 )}95 >96 {value}97 </div>98 );99}100
101export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {102 if (!api.rowIsLeaf(row) || !row.data) return null;103
104 const field = api.columnField(column, row);105
106 if (typeof field !== "number") return "-";107
108 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";109
110 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;111}112
113export const makePerfHeaderCell = (name: string, subname: string) => {114 return (_: Grid.T.HeaderParams<GridSpec>) => {115 return (116 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">117 <div>{name}</div>118 <div className="text-ln-text-light font-mono uppercase">{subname}</div>119 </div>120 );121 };122};When you use a React placeholder, render the placeholder value returned by api.useRowDrag, as shown below.
1function MarkerCell({ api, rowIndex }: Grid.T.CellRendererParams<GridSpec>) {2 const { props, placeholder } = api.useRowDrag({ rowIndex, placeholder: Placeholder });3
4 return (5 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>6 <DragHandleDots2Icon />7 {placeholder}8 </div>9 );10}Drag To External Applications
Since LyteNyte Grid utilizes the browser’s native drag and drop API, you can drag rows
into external applications. To enable this, set the data property in the
api.useRowDrag hook parameters, as shown below:
1function MarkerCell({ api, rowIndex, row }: Grid.T.CellRendererParams<GridSpec>) {2 const { props } = api.useRowDrag({3 rowIndex,4 data: { "row-content": { kind: "dt", data: JSON.stringify(row.data), type: "text/plain" } },5 });6
7 return (8 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>9 <DragHandleDots2Icon />10 </div>11 );12}The data property is a record of drag data. Each entry must be one of the following types, shown below.
The dt kind uses the native browser DataTransfer object. Use this kind when you want to support
dragging data into external applications.
1export type DragItemTransfer = {2 readonly kind: "dt";3 readonly type:4 | "text/plain"5 | "text/html"6 | "text/uri-list"7 | "application/json"8 | "text/csv"9 | "text/rtf"10 | ({} & string);11 readonly data: string;12};13
14export type DragItemSiteLocal = {15 readonly kind: "site";16 readonly data: unknown;17};In the demo below, users can drag rows into an external application that accepts the text/plain MIME
type. This includes most code editors. To try it out, open your text editor and drag a row into it.
Drag To External Applications
80 collapsed lines
1import "./demo.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import "@1771technologies/lytenyte-pro/pill-manager.css";4import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 ExchangeCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { DragHandleDots2Icon } from "@radix-ui/react-icons";16
17export interface GridSpec {18 readonly data: DEXPerformanceData;19}20
21const columns: Grid.Column<GridSpec>[] = [22 { id: "symbol", cellRenderer: SymbolCell, width: 250, name: "Symbol" },23 { id: "network", cellRenderer: NetworkCell, width: 220, hide: true, name: "Network" },24 { id: "exchange", cellRenderer: ExchangeCell, width: 220, hide: true, name: "Exchange" },25
26 {27 id: "change24h",28 cellRenderer: PercentCellPositiveNegative,29 headerRenderer: makePerfHeaderCell("Change", "24h"),30 name: "Change % 24h",31 type: "number,",32 },33
34 {35 id: "perf1w",36 cellRenderer: PercentCellPositiveNegative,37 headerRenderer: makePerfHeaderCell("Perf %", "1w"),38 name: "Perf % 1W",39 type: "number,",40 },41 {42 id: "perf1m",43 cellRenderer: PercentCellPositiveNegative,44 headerRenderer: makePerfHeaderCell("Perf %", "1m"),45 name: "Perf % 1M",46 type: "number,",47 },48 {49 id: "perf3m",50 cellRenderer: PercentCellPositiveNegative,51 headerRenderer: makePerfHeaderCell("Perf %", "3m"),52 name: "Perf % 3M",53 type: "number,",54 },55 {56 id: "perf6m",57 cellRenderer: PercentCellPositiveNegative,58 headerRenderer: makePerfHeaderCell("Perf %", "6m"),59 name: "Perf % 6M",60 type: "number,",61 },62 {63 id: "perfYtd",64 cellRenderer: PercentCellPositiveNegative,65 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),66 name: "Perf % YTD",67 type: "number",68 },69 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },70 {71 id: "volatility1m",72 cellRenderer: PercentCell,73 headerRenderer: makePerfHeaderCell("Volatility", "1m"),74 name: "Volatility 1M",75 type: "number",76 },77];78
79const base: Grid.ColumnBase<GridSpec> = { width: 80 };80
81
82const marker: Grid.ColumnMarker<GridSpec> = { on: true, cellRenderer: MarkerCell };83
84export default function RowDemo() {85 const ds = useClientDataSource({ data });86
87 return (88 <div89 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-cell-marker:px-0 ln-cell-marker:border-e ln-cell-marker:border-ln-border"90 style={{ height: 500 }}91 >92 <Grid columns={columns} columnBase={base} rowSource={ds} columnMarker={marker} />93 </div>94 );95}96
97function MarkerCell({ api, rowIndex, row }: Grid.T.CellRendererParams<GridSpec>) {98 const { props } = api.useRowDrag({99 rowIndex,100 data: { "row-content": { kind: "dt", data: JSON.stringify(row.data), type: "text/plain" } },101 });102
103 return (104 <div className="flex h-full w-full cursor-grab items-center justify-center" {...props}>105 <DragHandleDots2Icon />106 </div>107 );108}1[data-ln-drag-position="before"]::before {2 position: absolute;3 top: 0px;4 inset-inline-start: 0px;5 content: "";6 height: 2px;7 background-color: var(--ln-primary-50);8 width: 100%;9 z-index: 10;10}11
12[data-ln-drag-position="after"]::before {13 position: absolute;14 top: calc(var(--ln-row-height) - 2px);15 inset-inline-start: 0px;16 content: "";17 height: 2px;18 background-color: var(--ln-primary-50);19 width: 100%;20 z-index: 10;21}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11
12export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {13 if (!api.rowIsLeaf(row) || !row.data) return null;14
15 const ticker = row.data.symbolTicker;16 const symbol = row.data.symbol;17 const image = symbols[row.data.symbolTicker];18
19 return (20 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">21 <div>22 <img23 src={image}24 alt={`Logo for symbol ${symbol}`}25 className="h-full w-full overflow-hidden rounded-full"26 />27 </div>28 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">29 {ticker}30 </div>31 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>32 </div>33 );34}35
36export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {37 if (!api.rowIsLeaf(row) || !row.data) return null;38
39 const name = row.data.network;40 const image = networks[name];41
42 return (43 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">44 <div>45 <img46 src={image}47 alt={`Logo for network ${name}`}48 className="h-full w-full overflow-hidden rounded-full"49 />50 </div>51 <div className="w-full overflow-hidden text-ellipsis">{name}</div>52 </div>53 );54}55
56export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {57 if (!api.rowIsLeaf(row) || !row.data) return null;58
59 const name = row.data.exchange;60 const image = exchanges[name];61
62 return (63 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">64 <div>65 <img66 src={image}67 alt={`Logo for exchange ${name}`}68 className="h-full w-full overflow-hidden rounded-full"69 />70 </div>71 <div className="w-full overflow-hidden text-ellipsis">{name}</div>72 </div>73 );74}75
76export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {77 if (!api.rowIsLeaf(row) || !row.data) return null;78
79 const field = api.columnField(column, row);80
81 if (typeof field !== "number") return "-";82
83 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";84
85 return (86 <div87 className={tw(88 "h-ful flex w-full items-center justify-end tabular-nums",89 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",90 )}91 >92 {value}93 </div>94 );95}96
97export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {98 if (!api.rowIsLeaf(row) || !row.data) return null;99
100 const field = api.columnField(column, row);101
102 if (typeof field !== "number") return "-";103
104 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";105
106 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;107}108
109export const makePerfHeaderCell = (name: string, subname: string) => {110 return (_: Grid.T.HeaderParams<GridSpec>) => {111 return (112 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">113 <div>{name}</div>114 <div className="text-ln-text-light font-mono uppercase">{subname}</div>115 </div>116 );117 };118};Next Steps
- Row Pinning: Freeze rows at the top or bottom of the viewport.
- Row Full Width: Create rows that span the full width of the viewport.
- Row Selection: Select single or multiple rows.
